Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ import type * as presenceHttp from "../presenceHttp.js";
import type * as publication from "../publication.js";
import type * as rateLimits from "../rateLimits.js";
import type * as referrals from "../referrals.js";
import type * as remintDid from "../remintDid.js";
import type * as siteActions from "../siteActions.js";
import type * as siteAssets from "../siteAssets.js";
import type * as siteInternals from "../siteInternals.js";
Expand Down Expand Up @@ -142,7 +141,6 @@ declare const fullApi: ApiFromModules<{
publication: typeof publication;
rateLimits: typeof rateLimits;
referrals: typeof referrals;
remintDid: typeof remintDid;
siteActions: typeof siteActions;
siteAssets: typeof siteAssets;
siteInternals: typeof siteInternals;
Expand Down
4 changes: 3 additions & 1 deletion convex/didCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ async function createDIDRecord(
domain,
signer,
verifier: signer,
updateKeys: [verificationMethodId],
// Bare multibase, not the did:key URI — see lib/webvh.ts (didwebvh-ts 2.8
// compares updateKeys against the parsed keyMultibase by exact equality).
updateKeys: [address],
verificationMethods: [
{
id: "#key-0",
Expand Down
109 changes: 0 additions & 109 deletions convex/remintDid.ts

This file was deleted.

10 changes: 6 additions & 4 deletions convex/siteActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,14 @@ export const createSiteFromUpload = action({

const { privateKey, publicKeyMultibase } = await createSiteKey();
const signer = new SiteWebVHSigner(privateKey, publicKeyMultibase);
const verificationMethodId = signer.getVerificationMethodId();

const didResult = await createDID({
domain: hostname,
signer,
verifier: signer,
updateKeys: [verificationMethodId],
// Bare multibase, not the did:key URI — see lib/webvh.ts (didwebvh-ts 2.8
// compares updateKeys against the parsed keyMultibase by exact equality).
updateKeys: [publicKeyMultibase],
verificationMethods: [
{
id: "#key-0",
Expand Down Expand Up @@ -337,7 +338,6 @@ export const migrateVerifiedCustomDomain = action({
encryptionSecret
);
const signer = new SiteWebVHSigner(privateKey, record.key.publicKeyMultibase);
const verificationMethodId = signer.getVerificationMethodId();
const migratedDid = `did:webvh:${record.site.scid}:${hostname}`;
const currentLog = record.didLogEntries.map((entry) => JSON.parse(entry.entryJsonl));

Expand All @@ -347,7 +347,9 @@ export const migrateVerifiedCustomDomain = action({
verifier: signer,
domain: hostname,
controller: migratedDid,
updateKeys: [verificationMethodId],
// Bare multibase, not the did:key URI — see lib/webvh.ts (didwebvh-ts 2.8
// compares updateKeys against the parsed keyMultibase by exact equality).
updateKeys: [record.key.publicKeyMultibase],
verificationMethods: [
{
id: "#key-0",
Expand Down
103 changes: 103 additions & 0 deletions scripts/webvh-mint.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdir, rm } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { build } from "esbuild";

const outdir = "tmp/webvh-mint-test";

/**
* Exercises the real mint. didwebvh-ts verifies the log it just created, so a
* malformed `updateKeys` fails here exactly as it does in the browser.
*/
async function loadWebvh() {
await rm(outdir, { recursive: true, force: true });
await mkdir(outdir, { recursive: true });
await build({
entryPoints: ["src/lib/webvh.ts"],
outfile: `${outdir}/webvh.mjs`,
bundle: true,
platform: "node",
format: "esm",
target: "node20",
define: { "import.meta.env.VITE_WEBVH_DOMAIN": '"boop.ad"' },
plugins: [
{
name: "stub-capacitor",
setup(b) {
b.onResolve({ filter: /^@capacitor\/core$/ }, () => ({ path: "cap", namespace: "s" }));
b.onLoad({ filter: /.*/, namespace: "s" }, () => ({
contents: "export const Capacitor={isNativePlatform:()=>false};",
loader: "js",
}));
},
},
],
});
return import(pathToFileURL(`${process.cwd()}/${outdir}/webvh.mjs`).href);
}

/**
* Installs browser globals for the duration of `fn`, then restores them.
* defineProperty, not assignment: bun exposes a readonly `localStorage`.
* Restoring matters because bun shares globals across test files.
*/
function withBrowserGlobals(fn) {
const store = new Map();
const saved = ["localStorage", "window"].map((name) => ({
name,
descriptor: Object.getOwnPropertyDescriptor(globalThis, name),
}));

const localStorage = {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
};
const set = (name, value) =>
Object.defineProperty(globalThis, name, { value, configurable: true, writable: true });

set("localStorage", localStorage);
set("window", { location: { host: "boop.ad" }, localStorage });

const restore = () => {
for (const { name, descriptor } of saved) {
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
else delete globalThis[name];
}
};
return Promise.resolve(fn()).finally(restore);
}

const webvh = await loadWebvh();

test("createUserWebVHDid mints a verifiable did:webvh", async () => {
await withBrowserGlobals(async () => {
// Regression: updateKeys carried the `did:key:` URI while didwebvh-ts 2.8
// compares the parsed keyMultibase, so every mint threw
// "Key did:key:… is not authorized to update" — no account could get a DID.
const result = await webvh.createUserWebVHDid({
email: "someone@example.com",
subOrgId: "20ed9d43-2d31-44f8-9b02-2242c2749a58",
});

assert.match(result.did, /^did:webvh:/);
assert.ok(result.did.includes(":boop.ad:"), "must be minted on the current domain");
assert.equal(result.path, "user-20ed9d43-2d31-44");
assert.ok(result.didLogJsonl.trim().length > 0, "a log must be produced for the server");
});
});

test("the minted DID is not stale and yields an openable share URL", async () => {
await withBrowserGlobals(async () => {
const result = await webvh.createUserWebVHDid({
email: "someone@example.com",
subOrgId: "abc1234567890123456",
});
assert.equal(webvh.isStaleDidDomain(result.did), false, "a fresh mint must not re-trigger");
assert.match(
webvh.buildListResourceUrl(result.did, "l1"),
/^https:\/\/boop\.ad\/user-abc1234567890123\/resources\/list-l1$/
);
});
});
6 changes: 5 additions & 1 deletion src/lib/webvh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ export async function createUserWebVHDid(params: {
domain,
signer,
verifier: signer,
updateKeys: [signer.getVerificationMethodId()],
// Bare multibase, NOT the did:key URI. didwebvh-ts 2.8's isKeyAuthorized
// parses the proof's verificationMethod down to its keyMultibase and
// compares by exact equality, so a `did:key:z6Mk…` entry never matches and
// every mint fails with "Key … is not authorized to update".
updateKeys: [publicKeyMultibase],
verificationMethods: [
{
id: "#key-0",
Expand Down
Loading