npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wral/report-it

v0.3.2

Published

Lit web component for the Report It guided viewer-submission flow on WRAL.com: chat-style form walking, embedded My WRAL sign-in, submit-time media uploads, rendered against api-forms.

Readme

<wral-report-it>

Lit web component for the Report It guided viewer-submission flow on WRAL.com (DEV-1099): a chat-style walk through a form's flow tree, an embedded My WRAL sign-in gate, and submit-time media uploads, rendered against api-forms.

Usage

From the CDN (self-contained, registers the elements):

<script type="module"
	src="https://cdn.wral.com/@wral/report-it/v0/define.standalone.js"></script>
<wral-report-it api="https://api.wral.com/forms"></wral-report-it>

From npm:

import '@wral/report-it/define';        // registers wral-report-it,
                                        // wral-report-media, wral-code-input

Pre-sizing (avoid layout shift)

Keep this first in the host page's <head>:

<style>wral-report-it { display: block; min-height: 70vh; }</style>

Attributes and properties

| Name | Kind | Default | Purpose | | --- | --- | --- | --- | | api | attribute | https://api.wral.com/forms | api-forms base URL (dev: https://api.wral.com/dev/forms) | | client | property | — | Full I/O surface override (API + auth); wins over api. See src/demo/demo-client.mjs for the shape | | facts-api | attribute | — | api-user-facts v1 base on the same stage as api (prod https://api.wral.com/user-facts/v1, dev https://api.wral.com/dev/user-facts/v1). Unset, or on another stage: nothing is read from, offered to or written to My WRAL | | facts | attribute | all | The facts Report It may offer or record, separated by spaces or commas, any case: preferred_name phone_number street_address zipcode report_it_submission. facts="" turns them all off. An unknown name is ignored and named once in the console. A page republish changes it, no release, but cached copies of the page keep the old value for hours | | flow | attribute | — | Open one form directly by its formId (e.g. investigates). The host page's ?flow=<formId> does the same; the attribute wins |

Styling hooks: design tokens are read as var(--wral-…, fallback) with prototype-accurate fallbacks, so the component renders correctly with no token stylesheet on the page. --report-it-max-width overrides the column width (default 680px).

How a submission flows

  1. GET {api}/v1/forms (public) — one button per published form (forms with listed: false are served but not shown; see below).
  2. The chosen form's tree walks client-side: choice chips, collect cards (text/textarea/url/datetime/location/media/consent/select), the gate (My WRAL email + one-time code — skipped with a confirmation card when already signed in), redirect dead-ends, and the terminal review screen.
  3. Nothing uploads until the viewer confirms submit. Then: POST /v1/uploads (Bearer) → presigned S3 PUTs with per-file progress and retry (expired presigns re-mint per file) → PUT /v1/requests/{requestId} (idempotent — retries return the same reference) → confirmation.

Hand-offs, unlisted forms and deep links (DEV-1305)

A choice option may carry form (and optionally path) instead of next: choosing it switches to that form document and replays the path choice steps there first. News → "5 On Your Side" lands on the consumer form's complaint card, past its own "5 On Your Side or Investigates?" question. The submission is made against the target form, with the preset steps at the head of its path; a path through a hand-off option itself is never sent (the API rejects one).

The form the viewer came from is kept as an origin (src/lib/handoff.mjs), persisted with the draft: the transcript reads as one conversation, a reload rebuilds it, and Back from the first card after a hand-off returns to the choice that made it, skipping the preset steps. A hand-off to a form that is not loaded shows the load-error notice and stays put.

An unlisted form (listed: false, e.g. investigates) is fetched with the rest and reachable by hand-off or deep link, just not offered on the entry screen. ?flow=<formId> on the host page (or the flow attribute) opens a form directly; a draft for that form (or one handed off to or from it) resumes instead, and a draft for any other form yields to the link.

Fields

  • text fields pass inputMode and autocomplete from the form document through to the input, and each collect card is a real <form autocomplete="on">, so the browser can fill name, phone, address and zip from the viewer's saved details. Nothing is prefilled from My WRAL; answers reach My WRAL only as described below. (Autofill inside shadow DOM varies by browser — verify on iOS Safari and Android Chrome.)
  • A text field's pattern is checked before submit the way the server checks it (a zip gets its own message).
  • consent copy comes from the field's text. With requiredIf: "media" the checkbox appears only once a file is attached, is required only then, and is not sent without files.

Saving details to My WRAL (DEV-1335)

Off unless the host sets facts-api to an api-user-facts v1 base on the same stage as api. Then a submission that passed the sign-in card signed in (never an anonymous one, never a form without a gate) can do two things, both as the viewer, with the ID token, for the account the report was sent as only, and only when that is the account that passed the gate: if another tab signs in someone else before Submit, or after a reload the gate's email is not the signed-in token's, nothing is offered or recorded (factsEligible in src/lib/facts-offer.mjs):

  • Offer to save the viewer's own details (src/lib/user-facts.mjs). A field counts only when its key and its autocomplete are one of these pairs, and its answer can be shaped to what the API accepts (src/lib/fact-shapes.mjs; anything else is not offered, and the report keeps it as typed):

    | key / autocomplete | fact | saved as | box starts | | --- | --- | --- | --- | | name / name | preferred_name | single-spaced, at most 100 characters | checked | | phone / tel | phone_number | E.164, US and Canada only ((919) 555-0100 → +19195550100) | checked | | address / street-address | street_address | one single-spaced line, 1-200 characters with a digit and a letter; never cut | checked | | zip / postal-code | zipcode | the first five digits; 00000 is not offered | checked |

    Fill, never replace. An account created on this page load gets the v0.2 step before the confirmation, opening with the reference number. Any other account sees the confirmation at once; Report It then reads GET /user/<fact> (presence only, one uncached GET per fact) and offers, in a card under the confirmation that names the signed-in email, only what the profile lacks. Every box starts checked (since 0.3.2), and the question names exactly what one click on Save saves; the viewer unchecks what they do not want saved. Every save is create-only (If-None-Match: *): a value saved meanwhile wins and is reported as already in the account. Phone and street address are offered only where GET /fields marks them sensitive (kept out of GET /user and its browser caches; only their owner reads them, by name). A fact under an emergency write stop in GET /fields is not offered. Without GET /fields, a new account is offered its name and zip only (the v0.2 offer). A name or zip save clears the dashboard's wral_user_facts cache; nothing is kept in browser storage.

  • List the report on the account: one report_it_submission item, nonce = the requestId, value = the reference, written only while the confirmation that announces it is on screen (a new account's step holds it until Save or No thanks; a step left unanswered records nothing), with Remove it from my account. If the sign-in has ended or the device is now another account's, Remove it says so once, with no Try again. Never for Investigates or removal requests (by form, and by the routes studio+email:investigates and email:expunge). A pointer only: the record's account.accountId stays the authority on whose report it is.

Nothing the viewer typed is saved to My WRAL without their click. The sign-in card no longer says contact information "fills in automatically next time": Report It never fills a form in from My WRAL. The .client surface gains accountId(), accountEmail(), factFields(), readFactPresence(names, {expectSub}), saveFacts(items, {expectSub}), recordSubmission(ids, {expectSub, stillWanted}) and removeSubmission(ids, {expectSub}); saveUserFacts is gone. A .client without them gets no facts paths. In the demo: ?demoexisting=1 (existing accounts, name and zip on file), ?demofactsfail=1 (the first fact of the first save fails), ?demofactsread=fail|slow, ?demofields=old (an API before DEV-1335), ?demostop=<fact> (that fact under a write stop), ?demoswitch=1 (another account signs in after the submit).

Skipping the account (DEV-1270)

The sign-in card files its alternatives under a collapsed "Other ways to send this" disclosure — deliberately present but never the path of least resistance. It always offers the newsroom desk number. It offers an in-form anonymous submission only when the server advertises it: GET /v1/forms carries anonymous: {enabled, humanCheck}, the client records it (client.anonymousCapability()), and the route renders from that alone — never from anything baked into the page. A deployment with enabled: false, or an older API with no block at all, leaves the route dark and the phone number as the only anonymous option. A form suppresses the route regardless with allowAnonymous: false.

humanCheck is null today (no proof-of-human provider is configured; the API defends the route with per-caller limits). When a provider is configured server-side it becomes {provider, siteKey} and the component will render that widget in the confirmation card and pass its token as humanToken — the server is the only party that can verify a captcha, so the server is the only party that may ask for one.

The session itself: createAnonSession({formId, requestId}) posts to /v1/anon/sessions unauthenticated and holds the returned token; every auth-bearing call then presents it as the bearer in place of the My WRAL identity until endAnonSession() — called after a successful submit and on Start over, so a stale anonymous session can never ride along with a signed-in one.

Choosing it takes a confirmation card stating what the viewer gives up, with Sign in instead as the primary action. The choice is recorded on the gate step ({kind: 'gate', anonymous: true}), so it survives a reload; the session is minted at submit against the requestId, so an abandoned form leaves nothing behind, and a mid-upload expiry re-mints in place instead of opening the sign-in card.

Auth uses @wral/sdk-auth-audience directly (sendCode/verifyCode); requireAuth/authFetch are deliberately avoided — their 401 paths redirect to /login/, which would destroy an unsubmitted form. The bearer on uploads and submit is the viewer's Cognito ID token (not the access token the SDK's getAuthHeaders sends): same sub, but it also carries the verified email, which api-forms writes onto the record beside the account id (src/lib/id-token.mjs). A 401 mid-submit re-opens the gate in place and resumes where it parked.

Drafts

The answer stack persists to sessionStorage (wral-report-it:draft:v1) so a reload or app switch loses nothing. Attached files cannot persist — restored drafts show a re-attach notice for any files that were selected. Drafts are discarded when the form changed incompatibly, and flagged for review when the flowVersion moved but the answers still fit.

Development

npm install
npm test          # node --test over src/lib (the pure logic; the component's wiring,
                  # including the facts flow, is not covered: walk it in the dev harness)
npm run lint
npm run dev       # vite harness at http://localhost:5173

The harness runs against the live dev API by default (real My WRAL login — your email receives a real code) or fully offline with the in-memory demo client (any email, any six digits). Live dev pairs the dev forms API with the dev user-facts stage; it trusts the production Cognito pool, so use a test account there. The demo client serves the form documents in src/demo/fixtures/ (copies of api-forms' seeds), so hand-offs, ?flow=investigates and the files-only consent all work offline. Append ?demofail=1 to make the second upload fail once and exercise the retry UX. The Anonymous route checkbox (demo mode only, also settable with ?anon=1) makes the demo client advertise anonymous submission the way a real server does, lighting up the in-form option; unchecked it stays dark. In live-dev mode the route follows the dev API's own anonymous block, so a real anonymous submission can be exercised end to end against the dev content stage.

Release

npm version patch && git push && git push --tags — the v*.*.* tag pipeline publishes to npm and to cdn.wral.com/@wral/report-it/{vX.Y.Z,vX.Y,vX,latest}/. Repo variables: NPM_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, S3_BUCKET=cdn.wral.com, S3_PREFIX=@wral/report-it, AWS_DISTRIBUTION_ID.

The published pages pin the exact version (next section), so a release reaches wral.com only once the pin in pages/report-it.html is bumped and the page republished.

Pages

pages/report-it.html is the page template for both wral.com and the prod-wired staging page. It pins the component to an exact release (cdn.wral.com/@wral/report-it/vX.Y.Z/define.standalone.js, immutable); bump the pin and republish to roll a release out. Each page is a publisher publication whose template is an HTML asset in the editorial DAM (content: {}, template: {"$ref": "https://assets.wral.com/<asset id>"}):

| Page | api-forms stage | DAM asset | | --- | --- | --- | | www.wral.com/report-it/ (site wral.com) | prod (/forms) | 29d37933-02c6-45c6-9cd4-b13e2e453ef9 (v0.3.1 pin: the save card's why line and benefits link), the same asset as the staging page below. Earlier: 0931ef97-a746-4c22-8926-84ead8168fff (v0.3.0 pin, DEV-1335), 7744bd64-9379-484d-8c1b-0d02b669ac4a (v0.2.4 pin, menu fix), 4d0b598d-73e5-4449-b535-49dcc4246d55 (DEV-1109 cutover) | | /test/report-it/ (site staging.wral.com) | prod (/forms): real desk lists and the Studio queue | 29d37933-02c6-45c6-9cd4-b13e2e453ef9 (v0.3.1 pin; otherwise byte-identical to 0931ef97…). Earlier: 0931ef97-a746-4c22-8926-84ead8168fff (v0.3.0 pin, DEV-1335; otherwise byte-identical to 7744bd64…), 7744bd64-9379-484d-8c1b-0d02b669ac4a (v0.2.4 pin, approved logo, SEO metadata and OG image, component kept under the open site menu), 4d0b598d-73e5-4449-b535-49dcc4246d55 (before the menu fix), bd4f49c7-9155-489c-af2e-34913c109052 (v0.2.4 pin, before the metadata), defda1de-ba36-4250-ac7d-7c1d85bfc49e (same page on latest), daf0a481-9b33-4222-99c3-da4111fbf009 (facts-api, 2026-10-01), 39c268dd-06eb-46f9-a6bf-74d04af45bae (DEV-1300) | | /test/report-it-dev/ | dev (/dev/forms): the ReportIt_Dev list and the dev content stage | 3c78d978-bcd3-44f9-9906-c417db80de14, an older copy of the page that still loads the CDN latest alias (only the api attribute and its comment differ) |

To change a page:

  1. Upload the new HTML as a new asset. Use POST https://api.wral.com/dam/v1/asset/<new uuid>/upload-url with dam:write, then PUT the file with Content-Type: text/html.
  2. POST https://api.wral.com/publisher/v1/publications with publisher:write, sending {site, path, content: {}, template: {"$ref": …}, metadata: {httpStatusCode: 200}}.
  3. Invalidate the path on the site's CloudFront distribution (E2YSTU8KLPZ0ZR for wral.com, E2PUK7RNR73BP4 for staging), because the page is cached for about 6 h. The publisher trigger also invalidates a republished path on its own.

Leave the previous asset in place. Republishing with its $ref is the rollback.

Notes

  • wral-code-input + helpers are copied from webapps.wral-web.audience-login with one fix: the original's code-complete guard could never fire when typing left-to-right (only on paste). Worth upstreaming.
  • The county list for location fields is hardcoded (src/lib/counties.mjs) — the form model carries none; the component prefers field.counties if a form document ever ships it.
  • Required location fields need a county or a written place — one notch stricter than the server schema, deliberately.