diff --git a/packages/access-api/postmark/welcome.html b/packages/access-api/postmark/welcome.html
index 0c41fe1ef..fd8bfd155 100644
--- a/packages/access-api/postmark/welcome.html
+++ b/packages/access-api/postmark/welcome.html
@@ -1,5 +1,6 @@
Hi {{email}}! To complete your {{product_name}} registration, we just need to verify your email address.
+Before clicking the button below, please confirm the app you are trying to register is showing this phrase: {{match_phrase}}
diff --git a/packages/access-api/postmark/welcome.txt b/packages/access-api/postmark/welcome.txt
index 405fa4050..de519a5f5 100644
--- a/packages/access-api/postmark/welcome.txt
+++ b/packages/access-api/postmark/welcome.txt
@@ -1,7 +1,11 @@
Hi {{email}}! To complete your {{product_name}} registration,
we just need to verify your email address.
-Please visit the following link in your web browser:
+Please confirm the app you are trying to register is showing this phrase:
+
+{{match_phrase}}
+
+If it is, please visit the following link in your web browser:
{{action_url}}
diff --git a/packages/access-api/src/service/index.js b/packages/access-api/src/service/index.js
index 74fbd46f0..84e90148d 100644
--- a/packages/access-api/src/service/index.js
+++ b/packages/access-api/src/service/index.js
@@ -14,6 +14,7 @@ import { voucherClaimProvider } from './voucher-claim.js'
import { voucherRedeemProvider } from './voucher-redeem.js'
import * as uploadApi from './upload-api-proxy.js'
import { accessAuthorizeProvider } from './access-authorize.js'
+import { generateNoncePhrase } from '../utils/phrase.js'
import { accessDelegateProvider } from './access-delegate.js'
import { accessClaimProvider } from './access-claim.js'
import { providerAddProvider } from './provider-add.js'
@@ -195,6 +196,7 @@ export function service(ctx) {
const encoded = delegationToString(inv)
const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=recover`
+ const nonce = generateNoncePhrase()
// For testing
if (ctx.config.ENV === 'test') {
@@ -204,7 +206,9 @@ export function service(ctx) {
await ctx.email.sendValidation({
to: capability.nb.identity.replace('mailto:', ''),
url,
+ nonce,
})
+ return { matchPhrase: nonce }
}
),
},
diff --git a/packages/access-api/src/service/voucher-claim.js b/packages/access-api/src/service/voucher-claim.js
index 28e499aef..9d6caa744 100644
--- a/packages/access-api/src/service/voucher-claim.js
+++ b/packages/access-api/src/service/voucher-claim.js
@@ -1,6 +1,7 @@
import * as Server from '@ucanto/server'
import * as Voucher from '@web3-storage/capabilities/voucher'
import { delegationToString } from '@web3-storage/access/encoding'
+import { generateNoncePhrase } from '../utils/phrase.js'
/**
* @param {import('../bindings').RouteContext} ctx
@@ -41,10 +42,13 @@ export function voucherClaimProvider(ctx) {
}
const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}`
+ const nonce = generateNoncePhrase()
await ctx.email.sendValidation({
to: capability.nb.identity.replace('mailto:', ''),
url,
+ nonce,
})
+ return { matchPhrase: nonce }
})
}
diff --git a/packages/access-api/src/utils/email.js b/packages/access-api/src/utils/email.js
index cc2b6362c..313b49ed1 100644
--- a/packages/access-api/src/utils/email.js
+++ b/packages/access-api/src/utils/email.js
@@ -4,6 +4,7 @@ export const debug = () => new DebugEmail()
* @typedef ValidationEmailSend
* @property {string} to
* @property {string} url
+ * @property {string} nonce
*/
/**
@@ -44,6 +45,7 @@ export class Email {
product_name: 'Web3 Storage',
email: opts.to,
action_url: opts.url,
+ match_phrase: opts.nonce,
},
}),
})
diff --git a/packages/access-api/src/utils/phrase-words.json b/packages/access-api/src/utils/phrase-words.json
new file mode 100644
index 000000000..fd8fd08c6
--- /dev/null
+++ b/packages/access-api/src/utils/phrase-words.json
@@ -0,0 +1,988 @@
+[
+ "reflected",
+ "knowings",
+ "inkiest",
+ "gated",
+ "facilitates",
+ "firmly",
+ "priority",
+ "vicar",
+ "reconciliations",
+ "facades",
+ "earns",
+ "shy",
+ "glows",
+ "symptomatic",
+ "rumbles",
+ "tough",
+ "object",
+ "chip",
+ "privates",
+ "adjoin",
+ "punned",
+ "sizing",
+ "accidents",
+ "sculpture",
+ "commuted",
+ "proposals",
+ "pail",
+ "feeler",
+ "performed",
+ "footballs",
+ "sorority",
+ "revving",
+ "larked",
+ "tickling",
+ "continuations",
+ "repress",
+ "surlier",
+ "wildernesses",
+ "darting",
+ "overlaying",
+ "complexions",
+ "invoking",
+ "sinew",
+ "ampler",
+ "buys",
+ "survival",
+ "decoration",
+ "halos",
+ "inducement",
+ "snippets",
+ "walling",
+ "programmes",
+ "incoherence",
+ "incumbent",
+ "aquatics",
+ "embarrassments",
+ "displacement",
+ "bucketed",
+ "floor",
+ "rifled",
+ "handy",
+ "multiplicity",
+ "pants",
+ "bicycled",
+ "motorway",
+ "evacuates",
+ "minus",
+ "insect",
+ "backlogs",
+ "penknives",
+ "originator",
+ "photosynthesis",
+ "albeit",
+ "skewered",
+ "enthralls",
+ "departmental",
+ "subsidy",
+ "pinnacle",
+ "raves",
+ "down",
+ "culminating",
+ "overborne",
+ "authenticated",
+ "fluently",
+ "distend",
+ "horrifies",
+ "entitling",
+ "recursive",
+ "flutist",
+ "counteracting",
+ "licking",
+ "misdemeanors",
+ "allegiance",
+ "foretelling",
+ "burdening",
+ "maneuvering",
+ "amassed",
+ "moussed",
+ "felt",
+ "waist",
+ "implement",
+ "lab",
+ "palates",
+ "ambiance",
+ "always",
+ "registrations",
+ "endearing",
+ "siphon",
+ "viler",
+ "proclaiming",
+ "waterworks",
+ "gingham",
+ "guessed",
+ "confirmation",
+ "accord",
+ "humanities",
+ "migraines",
+ "inflammable",
+ "corroboration",
+ "applicant",
+ "revealing",
+ "bard",
+ "posted",
+ "omen",
+ "quota",
+ "decanter",
+ "plankton",
+ "gooier",
+ "executes",
+ "wagging",
+ "forest",
+ "passionate",
+ "charcoal",
+ "pile",
+ "aired",
+ "circa",
+ "tempts",
+ "fashion",
+ "devilled",
+ "deficiencies",
+ "cropping",
+ "fig",
+ "attorneys",
+ "lack",
+ "pretenses",
+ "chopped",
+ "promise",
+ "recounting",
+ "terminates",
+ "fabulous",
+ "interrupting",
+ "visions",
+ "memoirs",
+ "trebles",
+ "noblewoman",
+ "professor",
+ "journalism",
+ "summoned",
+ "enrolls",
+ "geographic",
+ "paused",
+ "epilogues",
+ "disks",
+ "rasping",
+ "penetration",
+ "transgresses",
+ "aphorisms",
+ "piranha",
+ "pigment",
+ "unconditionally",
+ "microfilmed",
+ "turntables",
+ "impish",
+ "vegetarianism",
+ "blanked",
+ "strata",
+ "exposed",
+ "traditional",
+ "phosphorus",
+ "menthol",
+ "forward",
+ "tethered",
+ "ensembles",
+ "sufficed",
+ "compacted",
+ "plush",
+ "infernos",
+ "solution",
+ "porting",
+ "inky",
+ "executors",
+ "jigsawed",
+ "steps",
+ "trawls",
+ "wilting",
+ "oddest",
+ "amazement",
+ "illegibly",
+ "jaywalked",
+ "mediums",
+ "tasks",
+ "pretensions",
+ "confiscates",
+ "predominance",
+ "watching",
+ "carving",
+ "immobilizes",
+ "fulfills",
+ "cutlets",
+ "eligible",
+ "funnel",
+ "seduce",
+ "crow",
+ "refreshment",
+ "circles",
+ "emirs",
+ "concluding",
+ "clink",
+ "denims",
+ "recoils",
+ "stall",
+ "hibernate",
+ "ponderous",
+ "goading",
+ "outcry",
+ "pursue",
+ "hesitating",
+ "matures",
+ "shoplifter",
+ "glamourous",
+ "commons",
+ "notched",
+ "debut",
+ "plagued",
+ "fiddling",
+ "assimilated",
+ "crown",
+ "parkway",
+ "staunchly",
+ "pilgrim",
+ "razes",
+ "garment",
+ "sobers",
+ "weeklies",
+ "caverns",
+ "spaded",
+ "consuming",
+ "variance",
+ "nicety",
+ "distract",
+ "assaults",
+ "enjoyment",
+ "engraves",
+ "endorse",
+ "embalms",
+ "seared",
+ "loudest",
+ "drys",
+ "tailspins",
+ "mushrooms",
+ "imagines",
+ "bolting",
+ "unwieldiest",
+ "dearer",
+ "sailboat",
+ "fruitless",
+ "toucan",
+ "welcomed",
+ "preface",
+ "redeems",
+ "participles",
+ "basil",
+ "postscripts",
+ "elastics",
+ "regulations",
+ "separates",
+ "calves",
+ "advertiser",
+ "overrides",
+ "taxpayer",
+ "shin",
+ "strive",
+ "resumption",
+ "volumes",
+ "reposing",
+ "coarsely",
+ "leapfrogging",
+ "masterminding",
+ "rereading",
+ "warpaths",
+ "unravels",
+ "chisels",
+ "carat",
+ "doles",
+ "imitate",
+ "scars",
+ "retail",
+ "relapses",
+ "flashback",
+ "adapts",
+ "monstrosities",
+ "clipboard",
+ "screech",
+ "reconstructs",
+ "milled",
+ "taker",
+ "entitled",
+ "kiln",
+ "overgrowing",
+ "mauled",
+ "mahoganies",
+ "baboons",
+ "bossed",
+ "shoaling",
+ "malaria",
+ "pelican",
+ "brow",
+ "homestead",
+ "wits",
+ "bookmark",
+ "varnished",
+ "issue",
+ "haze",
+ "translated",
+ "juggler",
+ "suffocate",
+ "thriftiest",
+ "buckled",
+ "secured",
+ "mystery",
+ "bums",
+ "brainiest",
+ "unsolved",
+ "overhaul",
+ "headquarters",
+ "distorter",
+ "airmailed",
+ "alibied",
+ "cataract",
+ "hoist",
+ "motherhood",
+ "goats",
+ "abbreviation",
+ "astonishing",
+ "retainer",
+ "underneaths",
+ "fortifies",
+ "seasonable",
+ "likeliest",
+ "thatcher",
+ "national",
+ "glue",
+ "prays",
+ "whiff",
+ "quailing",
+ "fragmentary",
+ "destroyed",
+ "centrally",
+ "stone",
+ "thrilled",
+ "defamed",
+ "fraternizing",
+ "aliasing",
+ "adulterates",
+ "slashing",
+ "inhabiting",
+ "strutted",
+ "interestingly",
+ "localized",
+ "sketching",
+ "backbones",
+ "differentiated",
+ "resignation",
+ "chid",
+ "industry",
+ "superbest",
+ "moves",
+ "weekended",
+ "festivity",
+ "archbishops",
+ "vigilance",
+ "excitement",
+ "tinning",
+ "inaction",
+ "masculine",
+ "lilted",
+ "blithe",
+ "miraculously",
+ "abetting",
+ "affirms",
+ "respecting",
+ "connoisseurs",
+ "holidays",
+ "bemuses",
+ "tricycles",
+ "battleships",
+ "tenser",
+ "temporal",
+ "another",
+ "established",
+ "intersection",
+ "mariner",
+ "dogged",
+ "fascinates",
+ "scheduled",
+ "strides",
+ "studied",
+ "redundant",
+ "parlor",
+ "oversee",
+ "overburdens",
+ "teeter",
+ "synthetics",
+ "comprehension",
+ "stockier",
+ "berry",
+ "stoves",
+ "covenant",
+ "fronts",
+ "bashes",
+ "syringe",
+ "poster",
+ "clips",
+ "how",
+ "exulting",
+ "microwave",
+ "honeymoons",
+ "quarterbacking",
+ "today",
+ "terrain",
+ "engrave",
+ "constitutional",
+ "discuss",
+ "uneasier",
+ "disruptive",
+ "photographed",
+ "hoofing",
+ "breadths",
+ "taped",
+ "install",
+ "benefits",
+ "altars",
+ "prolonged",
+ "bottoming",
+ "nationalize",
+ "filter",
+ "bleed",
+ "musically",
+ "captain",
+ "roles",
+ "sister",
+ "neurology",
+ "ethos",
+ "stability",
+ "speeds",
+ "distilled",
+ "bedlams",
+ "impatiences",
+ "bother",
+ "cornflakes",
+ "sirups",
+ "hypothesize",
+ "professions",
+ "underground",
+ "forgot",
+ "titled",
+ "researched",
+ "cartoonist",
+ "shirks",
+ "naughtiest",
+ "chidden",
+ "scary",
+ "musts",
+ "unblocking",
+ "disposition",
+ "stowaways",
+ "blemish",
+ "ripples",
+ "crackled",
+ "proponents",
+ "disqualify",
+ "fretful",
+ "unicorns",
+ "slewed",
+ "genuses",
+ "unskilled",
+ "eclipsed",
+ "sprinters",
+ "revengeful",
+ "provably",
+ "quailed",
+ "bulled",
+ "prophetic",
+ "timer",
+ "yanking",
+ "epic",
+ "inflammations",
+ "decomposes",
+ "renewal",
+ "unanimously",
+ "autumns",
+ "amended",
+ "guests",
+ "hared",
+ "firmware",
+ "itchiest",
+ "comedians",
+ "huskily",
+ "integrals",
+ "builder",
+ "golfed",
+ "craziness",
+ "aphorism",
+ "topical",
+ "bestows",
+ "transparency",
+ "whisks",
+ "combated",
+ "maltreats",
+ "behead",
+ "reforms",
+ "leftmost",
+ "deepened",
+ "admires",
+ "attachés",
+ "clowning",
+ "fair",
+ "embargoed",
+ "tenures",
+ "ventilate",
+ "broods",
+ "mortgaging",
+ "hurrayed",
+ "indorsements",
+ "retired",
+ "protracted",
+ "welder",
+ "lingering",
+ "stepping",
+ "sapped",
+ "chairmen",
+ "rhinoceroses",
+ "trapezoids",
+ "candled",
+ "brinier",
+ "swarming",
+ "pawnbroker",
+ "colanders",
+ "electronic",
+ "oscillated",
+ "lazier",
+ "biting",
+ "runny",
+ "scanties",
+ "vacationed",
+ "upside",
+ "italicizing",
+ "impunity",
+ "airstrips",
+ "redundancy",
+ "rectified",
+ "violets",
+ "kiting",
+ "condemnation",
+ "bayoneted",
+ "revolve",
+ "condenses",
+ "pledged",
+ "transitive",
+ "collating",
+ "bargaining",
+ "comprehensible",
+ "steaks",
+ "immoral",
+ "yawning",
+ "deer",
+ "result",
+ "female",
+ "cartons",
+ "tugging",
+ "memorably",
+ "slotted",
+ "overlie",
+ "subset",
+ "righting",
+ "sundaes",
+ "infant",
+ "manor",
+ "Tuesday",
+ "balance",
+ "massacring",
+ "oftener",
+ "exists",
+ "pallid",
+ "buns",
+ "drafted",
+ "pension",
+ "absconded",
+ "torpedoes",
+ "shampoos",
+ "dire",
+ "reinforced",
+ "tablespoonsful",
+ "triumphing",
+ "menus",
+ "sillier",
+ "blessing",
+ "dormant",
+ "shrapnel",
+ "rodent",
+ "trillion",
+ "shrimp",
+ "sixtieths",
+ "notions",
+ "vended",
+ "representative",
+ "prevented",
+ "accede",
+ "resign",
+ "cutback",
+ "hugely",
+ "chalice",
+ "socialized",
+ "tanning",
+ "tee",
+ "tablespoonful",
+ "heavier",
+ "gazetted",
+ "description",
+ "reals",
+ "fluffs",
+ "kisses",
+ "daubs",
+ "diapers",
+ "kayaking",
+ "consisting",
+ "encore",
+ "indefensible",
+ "touches",
+ "exhilarated",
+ "bloodshed",
+ "devotees",
+ "displease",
+ "stinging",
+ "expanded",
+ "trailed",
+ "satisfaction",
+ "eked",
+ "imprinting",
+ "mystified",
+ "mountaineering",
+ "blindingly",
+ "artists",
+ "swoon",
+ "totalled",
+ "accent",
+ "dogging",
+ "duration",
+ "cleavers",
+ "lusted",
+ "digress",
+ "educating",
+ "walled",
+ "hanging",
+ "gnawn",
+ "ordination",
+ "flicks",
+ "stylistic",
+ "ditties",
+ "financial",
+ "infertile",
+ "pavilions",
+ "millionth",
+ "doubting",
+ "heirlooms",
+ "vision",
+ "cobs",
+ "patches",
+ "proletariat",
+ "potency",
+ "majesty",
+ "evoked",
+ "limitless",
+ "breakthrough",
+ "abruptly",
+ "homeland",
+ "recovers",
+ "foolishly",
+ "ascends",
+ "rainier",
+ "poems",
+ "possesses",
+ "recycled",
+ "foresting",
+ "cuticle",
+ "repeats",
+ "choppered",
+ "holiness",
+ "remnants",
+ "humiliations",
+ "division",
+ "assort",
+ "ceremonies",
+ "browned",
+ "debilitates",
+ "abstraction",
+ "felted",
+ "jackknifes",
+ "kickoffs",
+ "examines",
+ "squids",
+ "beloveds",
+ "military",
+ "uniquer",
+ "withdrawal",
+ "lately",
+ "recovery",
+ "muggy",
+ "paragraphs",
+ "tiptoed",
+ "encounter",
+ "effects",
+ "southerly",
+ "acceptably",
+ "ploughed",
+ "brim",
+ "caricatured",
+ "grub",
+ "yanked",
+ "discern",
+ "algorithm",
+ "tarried",
+ "connecter",
+ "facilitated",
+ "history",
+ "fractures",
+ "decreeing",
+ "informally",
+ "terrors",
+ "fillet",
+ "wisp",
+ "hat",
+ "layout",
+ "jingle",
+ "congealed",
+ "relativity",
+ "fossilize",
+ "swift",
+ "serums",
+ "lineage",
+ "shamming",
+ "engrossing",
+ "recite",
+ "authorship",
+ "miserable",
+ "tender",
+ "ancientest",
+ "wean",
+ "ravine",
+ "matriculation",
+ "reticent",
+ "border",
+ "highly",
+ "unfriendliest",
+ "luxurious",
+ "are",
+ "gashed",
+ "trouser",
+ "retreating",
+ "deadening",
+ "overstepped",
+ "thuds",
+ "interrupted",
+ "corporals",
+ "oscillates",
+ "aggregating",
+ "oversees",
+ "mobilizes",
+ "shoos",
+ "enthusiasms",
+ "vertebrate",
+ "grill",
+ "dullest",
+ "conspicuous",
+ "quilt",
+ "stenographers",
+ "clarify",
+ "worker",
+ "tenanted",
+ "eluded",
+ "misprinting",
+ "bibs",
+ "protests",
+ "depicting",
+ "muting",
+ "raisins",
+ "anchovies",
+ "residence",
+ "shoestrings",
+ "artificially",
+ "skinning",
+ "mysteriously",
+ "gullets",
+ "advisory",
+ "ellipsis",
+ "directed",
+ "skied",
+ "chum",
+ "shirted",
+ "compatibility",
+ "alternating",
+ "cricket",
+ "guises",
+ "authorize",
+ "sheathed",
+ "chapel",
+ "motivating",
+ "flouncing",
+ "runniest",
+ "polkas",
+ "lament",
+ "starting",
+ "sizeable",
+ "shoeing",
+ "recuperates",
+ "dished",
+ "utensils",
+ "perplexities",
+ "slithering",
+ "grimes",
+ "leaps",
+ "leans",
+ "mistook",
+ "lineages",
+ "unheard",
+ "chemicals",
+ "udder",
+ "payroll",
+ "attesting",
+ "rosaries",
+ "wiggle",
+ "resembled",
+ "rebuffing",
+ "signature",
+ "predicated",
+ "reversals",
+ "hamsters",
+ "semblance",
+ "inferences",
+ "disagreed",
+ "sloshed",
+ "dullness",
+ "contested",
+ "shovelled",
+ "dedicating",
+ "extolls",
+ "liturgy",
+ "superstructure",
+ "entirely",
+ "spiked",
+ "befriended",
+ "embark",
+ "hankers",
+ "eczema",
+ "festoons",
+ "optimizes",
+ "racketed",
+ "conventionally",
+ "subsides",
+ "strictly",
+ "blaspheme",
+ "unmarked",
+ "lugging",
+ "integrated",
+ "textured",
+ "succored",
+ "warns",
+ "convoys",
+ "ousters",
+ "technological",
+ "equalize",
+ "jelled",
+ "lieu",
+ "ingests",
+ "synchronizing",
+ "intelligence",
+ "digitize",
+ "meddlers",
+ "bicycling",
+ "verier",
+ "quilting",
+ "stormed",
+ "nettle",
+ "weest",
+ "sarcasms",
+ "fiancé",
+ "clumsier",
+ "wrest",
+ "taxied",
+ "greenbacks",
+ "traders",
+ "brawniest",
+ "tenderized",
+ "travesties",
+ "postulated",
+ "reunions",
+ "cached",
+ "raspberry",
+ "commended",
+ "rhythmic",
+ "meteoric",
+ "commence",
+ "withdrawn",
+ "acclimatized",
+ "nickname",
+ "our",
+ "saves",
+ "slag",
+ "casualties",
+ "psychologically",
+ "transient",
+ "composure",
+ "sears",
+ "tinselling",
+ "discharging",
+ "wand",
+ "foreigners",
+ "bake",
+ "gulch",
+ "clamped",
+ "restarted",
+ "elopement",
+ "spontaneous",
+ "vaccinates",
+ "observers",
+ "highbrow",
+ "stores",
+ "blindest",
+ "periled",
+ "travelings",
+ "browns",
+ "populations",
+ "entailed",
+ "advises",
+ "unlike",
+ "angled",
+ "teammates",
+ "bequest",
+ "disburses",
+ "ordinance",
+ "wider",
+ "figures",
+ "decking",
+ "overwhelmingly",
+ "satellited",
+ "piranhas",
+ "engraving",
+ "tingles",
+ "hiatuses",
+ "presumed",
+ "toadstool",
+ "commanders",
+ "braiding",
+ "zinced",
+ "enshrining",
+ "serviced",
+ "pampers",
+ "concentrated",
+ "collared",
+ "spinal",
+ "unilateral",
+ "covenanting",
+ "ekes",
+ "ridden",
+ "embroidering",
+ "enlistments",
+ "tiptoes",
+ "facade",
+ "embryonic",
+ "fault",
+ "filming",
+ "Sunday",
+ "myths",
+ "chapters",
+ "lunched",
+ "workers"
+]
diff --git a/packages/access-api/src/utils/phrase.js b/packages/access-api/src/utils/phrase.js
new file mode 100644
index 000000000..bd42ca9ab
--- /dev/null
+++ b/packages/access-api/src/utils/phrase.js
@@ -0,0 +1,48 @@
+import words from './phrase-words.json'
+
+/* above can be gathered by hand or e.g.
+sudo apt install wamerican-small jq
+
+numWords=1024
+# HT: https://stackoverflow.com/a/15065490/179583
+shuf -n $numWords <(grep -v \' /usr/share/dict/words) | \
+# HT: https://stackoverflow.com/a/34576956/179583
+jq --raw-input | jq --slurp > src/utils/phrase-words.json
+*/
+
+// TODO: I can't get this to work, but it'd be better to use this!
+// import { randomInt } from 'node:crypto'
+
+/**
+ *
+ * @param {number} max
+ * @returns {number}
+ */
+function randomInt(max) {
+ const array = new Uint32Array(1)
+ const random = self.crypto.getRandomValues(array)[0]
+ // 0xFFFFFFFF is the max value of a Uint32
+ return Math.floor((random / 0xff_ff_ff_ff) * max)
+}
+
+const DEFAULT_ENTROPY = 50
+const entropyPerWord = Math.log2(words.length)
+
+function randomWord() {
+ const randomIdx = randomInt(words.length)
+ return words[randomIdx]
+}
+
+/**
+ *
+ * @param {number} [entropy]
+ * @returns {string}
+ */
+export function generateNoncePhrase(entropy = DEFAULT_ENTROPY) {
+ const phrase = []
+ let nWordsNeeded = Math.ceil(entropy / entropyPerWord)
+ while (nWordsNeeded--) {
+ phrase.push(randomWord())
+ }
+ return phrase.join(' ')
+}
diff --git a/packages/access-api/tsconfig.json b/packages/access-api/tsconfig.json
index 1facffca5..4b151d9d6 100644
--- a/packages/access-api/tsconfig.json
+++ b/packages/access-api/tsconfig.json
@@ -6,7 +6,14 @@
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
- "include": ["src", "scripts", "test", "package.json", "sql"],
+ "include": [
+ "src",
+ "scripts",
+ "test",
+ "sql",
+ "package.json",
+ "src/utils/phrase-words.json"
+ ],
"exclude": ["**/node_modules/**"],
"references": [
{ "path": "../access-client" },
diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js
index 516653236..65a35bc30 100644
--- a/packages/access-client/src/agent.js
+++ b/packages/access-client/src/agent.js
@@ -335,6 +335,7 @@ export class Agent {
* @param {string} email
* @param {object} [opts]
* @param {AbortSignal} [opts.signal]
+ * @param {import('./types').ValidationPhraseHandler} [opts.onPhrase]
*/
async recover(email, opts) {
const inv = await this.invokeAndExecute(Space.recoverValidation, {
@@ -346,6 +347,10 @@ export class Agent {
throw new Error('Recover validation failed', { cause: inv })
}
+ if (inv?.matchPhrase) {
+ opts?.onPhrase?.(inv.matchPhrase)
+ }
+
const spaceRecover =
/** @type {Ucanto.Delegation<[import('./types').SpaceRecover]>} */ (
await this.#waitForDelegation(opts)
@@ -433,6 +438,7 @@ export class Agent {
* @param {string} email
* @param {object} [opts]
* @param {AbortSignal} [opts.signal]
+ * @param {import('./types').ValidationPhraseHandler} [opts.onPhrase]
* @param {Ucanto.DID<'key'>} [opts.space] - space to register
* @param {Ucanto.DID<'web'>} [opts.provider] - provider to register - defaults to this.connection.id
*/
@@ -471,6 +477,10 @@ export class Agent {
throw new Error('Voucher claim failed', { cause: inv })
}
+ if (inv?.matchPhrase) {
+ opts?.onPhrase?.(inv.matchPhrase)
+ }
+
const voucherRedeem =
/** @type {Ucanto.Delegation<[import('./types').VoucherRedeem]>} */ (
await this.#waitForDelegation(opts)
diff --git a/packages/access-client/src/types.ts b/packages/access-client/src/types.ts
index e011118a4..31724c18b 100644
--- a/packages/access-client/src/types.ts
+++ b/packages/access-client/src/types.ts
@@ -29,6 +29,7 @@ import * as Ucanto from '@ucanto/interface'
import type {
Abilities,
+ NoncePhrase,
SpaceInfo,
SpaceRecover,
SpaceRecoverValidation,
@@ -264,6 +265,8 @@ export type DelegationOptions = SetRequired & {
audienceMeta: AgentMeta
}
+export type ValidationPhraseHandler = (phrase: NoncePhrase) => undefined
+
/**
* Utility types
*/
diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts
index 415d40048..2464b0ca2 100644
--- a/packages/capabilities/src/types.ts
+++ b/packages/capabilities/src/types.ts
@@ -82,6 +82,11 @@ export type StoreList = InferInvokedCapability
// Top
export type Top = InferInvokedCapability
+export type NoncePhrase = string
+export interface HasValidationNonce {
+ phrase: NoncePhrase
+}
+
export type Abilities = TupleToUnion
export type AbilitiesArray = [