Browser-only ESM TypeScript package @proof.com/proof-vc-web. Ships one Web Component, <proof-verify-id>, that consumers drop into any markup (React, Vue, Svelte, plain HTML) to start a Proof verifiable-credentials flow on click. Depends on @proof.com/proof-vc-common for init, getAuthorizationRequestURL, and the transactionData builder.
- No
node:*or Node-only imports undersrc/. Browser-only package, no Node entry. Type-only imports (import type/export type *) are safe —verbatimModuleSyntax: trueerases them at emit. - Keep the SSR guards in
src/proof_verify_id.tsandsrc/index.ts. Without them, three module-scope sites throw when an SSR framework evaluates the import in Node:class extends HTMLElement→ guarded by the conditionalBaseconstant.new CSSStyleSheet()→ guarded bytypeof CSSStyleSheet !== "undefined".customElements.define(...)→ guarded bytypeof customElements !== "undefined".
- Never reference
src/react.tsfromsrc/index.ts. It's the types-only sub-path entry (@proof.com/proof-vc-web/react); importing it from the main entry forces every non-React consumer to install@types/react. - Prompt before publishing. Never bump the version, push tags, create a Release, or trigger the publish workflow without explicit confirmation — publishes are permanent.
- Run
yarn check-allbefore any commit or push. It's this repo's "tests + lint": format, lint, typecheck, publint.check-alldoes not coversite/(roottsconfig.jsonexcludes it). Sincesite/imports parentsrc/and pullsproof-vc-common, changes tosrc/, dependencies, orsite/can break the site's CI even whencheck-allpasses — so also run the site's checks before commit:cd site && yarn format:check && yarn lint:check && yarn typecheck && yarn build. - Keep
yarn publinton--pack npm.--pack autopicks yarn-1 mode and reports false-positive "file not published" errors. - Don't lower
engines.nodebelow>=22.0.0. Matches proof-vc-common. - Never silence lint with
eslint-disable. Fix the underlying issue, not the warning. The only sanctioned exception is a reviewed config override (e.g. the per-fileno-namespacerule forsrc/react.tsineslint.config.js) — not inline disable comments.
| Command | Purpose |
|---|---|
yarn check-all |
Full check: format, lint, typecheck, publint |
yarn build |
tsc emit to dist/ |
yarn typecheck |
tsc --noEmit |
yarn lint:check |
eslint, no fix |
yarn lint |
eslint --fix |
yarn format:check |
prettier --check |
yarn format |
prettier --write |
yarn publint |
publint --pack npm (keep the flag) |
cd site && yarn dev |
Webpack dev server on http://localhost:4000 |
Run tooling through yarn, not npx — the binaries are local devDependencies. Use the script when one exists (yarn format:check); otherwise run the local binary directly (yarn prettier --check <file>).
| File | Role |
|---|---|
src/index.ts |
Public entry. Registers <proof-verify-id> as an import side effect; re-exports init, transactionData, types. |
src/proof_verify_id.ts |
The ProofVerifyId class extending HTMLElement (via guarded Base). |
src/styles.ts |
CSS as a tagged template literal. No SCSS, no sass build step. |
src/react.ts |
declare module "react" JSX augmentation. Sub-path types entry only. |
site/ |
Local test app. Webpack-served, imports parent source via ../../src/index.ts. Not published, not a workspace. |
docs/ |
README assets (button.svg, buttons.svg). |
Open shadow root holding one <button>, the seal <svg>, and a <span> label (omitted when size="icon").
- A single module-scope
CSSStyleSheetbuilt fromstyles.tsis adopted into every shadow root viaadoptedStyleSheets. observedAttributes = ["size"]— onlysizedrives structural state (label visibility +aria-label).themeis pure CSS.nonce,state,login-hint,transaction-dataare read at click time, not observed.- No
connectedCallback.attributeChangedCallbackfires on parser upgrade,setAttribute, and framework attribute updates — covering React/Vue/Svelte. - The
clicklistener is on the host (this), not the inner<button>. A real button click bubbles up to the host (click iscomposed), and a programmatichost.click()fires on the host directly — so a consumer's form can trigger the flow withel.querySelector("proof-verify-id").click(), no shadow-root piercing. (Listening on the inner button would misshost.click(), since events don't propagate down into the shadow tree.) #navigate()resolves the redirect URL, then setswindow.location.href(a nullish URL aborts). It isdisabled/ busy while pending. The URL comes from either theresolveAuthorizationUrlproperty (if set) or#buildAuthorizationUrl()— see below.
resolveAuthorizationUrl?: () => string | null | undefined | Promise<...> is a property-only escape hatch. When set, #navigate() awaits it and redirects to its result instead of calling #buildAuthorizationUrl(); the nonce / state / login-hint / transaction-data inputs are ignored and nonce is not required. A nullish return aborts the redirect (the button re-enables); a throw propagates with the button restored. Use it for flows where the URL is produced elsewhere (e.g. a pushed authorization request made by the consumer). JS/JSX path only (it's a function); in TSX it's set as a property — works on React 19, use a ref on older React.
Both are read property-first, attribute-fallback in #buildAuthorizationUrl():
nonce— read asthis.nonce || this.getAttribute("nonce").nonceis a built-in IDL property, so frameworks assign it as a property (never callingsetAttribute); property-first is required, orgetAttributereturns null.transaction_data(AuthorizationRequestParams.transaction_dataacceptsTransactionData | string):- Property
el.transactionData = transactionData.paymentMandate({...})— structured, typed, from proof-vc-common factories. JS/JSX path. - Attribute
transaction-data="..."— pre-encoded string, for HTML-only consumers.
- Property
Themes (dark / gray / outline / primary) and sizes (small / medium / large / icon) apply via :host([theme="..."]) / :host([size="..."]). Grouped selectors make the unattributed element match the explicit default:
:host(:not([theme])) button,
:host([theme="primary"]) button { ... }Same pattern for size. Default theme primary, default size medium.
@proof.com/proof-vc-web/react is types-only: dist/react.js is an empty module (it only makes the export valid); the .d.ts augments React.JSX.IntrinsicElements so <proof-verify-id ...> typechecks in TSX. Consumers opt in via "types": ["@proof.com/proof-vc-web/react"], a triple-slash reference, or a type-only side-effect import.
verbatimModuleSyntax: true— useimport type/export type.noUncheckedIndexedAccess: true— indexing returnsT | undefined; use!only when access is guaranteed safe.exactOptionalPropertyTypes: true— spread optional fields conditionally:...(state !== null && { state }).- Local imports use the
.tsextension (rewriteRelativeImportExtensionsrewrites to.json emit). - DOM/JSX attribute names are kebab-case (
login-hint,transaction-data); the typed transaction-data JS property is camelCase (transactionData). src/react.tsusesnamespace JSX(the only legal React JSX augmentation); an ESLint per-file override disables@typescript-eslint/no-namespacethere.
src/styles.ts: add:host([theme="<name>"]) button { ... }withbackground-color,color, optionalborder, and&:hover.src/react.ts: add<name>to thetheme?union inProofVerifyIdJSXAttributes.site/public/index.html: add a demo row (optional).
Themes are pure CSS — no change in proof_verify_id.ts.
src/styles.ts: add:host([size="<name>"]) button { ... }withheight,padding,font-size,gap,border-radius,svg { width/height }.src/react.ts: extend thesize?union.- Only if the size needs special label behavior (e.g. icon-only): update
#syncSize()inproof_verify_id.ts.
Use src/react.ts as the template:
- Create
src/<framework>.tswith the framework's module augmentation. - Add to
package.jsonexports:"./<framework>": { "types": "./dist/<framework>.d.ts" }. - If its TS rules trip
no-namespace, add the file to the per-file ESLint override ineslint.config.js. - If a
@types/...package is needed at build, add it todevDependenciesand as an optional peer.
Replace the SEAL_SVG string in proof_verify_id.ts (assigned to button.innerHTML). It's currentColor-filled, so theming flows through. The same path is mirrored in docs/button.svg and docs/buttons.svg (<symbol id="seal">) — update those too or the docs drift.
.github/workflows/ci.yml, on push and PR to main. One job per check, run in parallel: format, lint, typecheck, build, publint, site.
builduploadsdist/as an artifact;publintneeds: buildand downloads it (publint resolvesexportsagainst the packed tarball, so it needs the build output).siteinstalls root deps thensite/deps —site/imports parentsrc/, which pullsproof-vc-commonfrom the rootnode_modules— then runs the site'sformat:check,lint:check,typecheck,build.- Workflow-level
permissions: contents: read; the publish job addsid-token: writefor OIDC.
Dependabot (.github/dependabot.yml, weekly) watches root npm, site/ npm, and github-actions. Minor/patch updates are grouped into one PR per ecosystem; majors get individual PRs.
Prompt before publishing (Hard Rule 4).
- Auth: npm Trusted Publishing via OIDC (no
NPM_TOKEN). - Trigger: GitHub Release published →
.github/workflows/publish.yml. - Registry: https://www.npmjs.com/package/@proof.com/proof-vc-web
main is branch-protected: direct pushes are rejected. Bump on a branch, merge the PR, then create the Release against the exact merged commit SHA.
- Bump on a branch (no auto-tag from npm — the tag is created by
gh release createin step 4):git switch -c release-X.Y.Z origin/main npm version patch --no-git-tag-version # or minor / major; writes package.json only git commit -am "Release X.Y.Z" git push -u origin release-X.Y.Z
- Open a PR. Approve and merge in the GitHub UI.
- Locate the merged commit SHA on
mainby grepping for the release commit subject:Expect exactly one match. If zero matches, the PR isn't merged yet. If multiple, narrow the grep to the exact subject (e.g.git fetch origin main SHA=$(git log origin/main --grep='Release X.Y.Z' --format=%H -n 1) echo "$SHA" # sanity-check before using
--grep='^Release X\.Y\.Z$'). - Create the Release against that SHA —
gh release createcreates the tag automatically when it doesn't exist:gh release create vX.Y.Z --target "$SHA" --generate-notes
The Release triggers publish.yml: check suite → tag must match package.json → npm publish --provenance --access public.
Never git push --follow-tags to main: the commit is rejected but the tag still pushes, stranding it on an unmerged commit. Delete a stray tag with git push --delete origin vX.Y.Z.
A 404 after a successful provenance step means npm rejected auth (it returns 404 instead of 403 to hide existence). Check the Trusted Publisher config at https://www.npmjs.com/package/@proof.com/proof-vc-web/access:
- Organization matches the GitHub org (case-sensitive)
- Repository:
proof-vc-web - Workflow filename:
publish.yml - Environment: empty (unless the workflow uses GitHub Environments)
yarn.lockis the only lockfile (nopackage-lock.json).- Scope is
@proof.com(with the dot), not@proof. - CI runs on the Node version in
.node-version. @proof.com/proof-vc-commonisyarn link-ed during local dev;package.jsonalready depends on the published version, the link just shadows it.tsconfig.jsonexcludessite,dist,node_modules— don't remove fromexclude; the site imports parentsrc/, which would otherwise causerootDirfailures.dist/is build output;tscoverwrites but doesn't delete stale files. Don't add a clean step without asking.