chore(analytics): configure posthog session replay masking (#1428)
* chore(analytics): configure posthog session replay masking Move session replay from the mask-everything default to pattern-based masking in lib/analytics/replay-masking.ts: currency-shaped text, person-/organisationsnummer (rendered and typed) and password inputs are masked; other interface text and typed input is recorded for debugging. data-ph-mask keeps force-masking tagged PII and data-ph-unmask is still honored for chrome. Privacy policy, RoPA and decision log updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(analytics): address review comments on replay masking PR Bump the privacy policy's visible last-updated date to 2026-08-06 and add the conventional vi.clearAllMocks() beforeEach to the replay-masking tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
902b3ee986
commit
a5c10e441a
+11
-3
@@ -605,6 +605,9 @@ processing_activities:
|
||||
data_categories:
|
||||
- user.contact.email # person property via identify(), aldrig i event-properties
|
||||
- user.name # profiles.full_name
|
||||
# Sessionsinspelningen visar synlig text och tangentbordsinmatning,
|
||||
# förutom maskerade fält (belopp, person-/orgnummer, lösenord samt
|
||||
# data-ph-mask-taggade element), se security_measures nedan.
|
||||
- user.behavior # sidvisningar, klick, händelser, sessionsinspelning
|
||||
- user.device # user agent, skärmstorlek, IP (trunkeras av PostHog)
|
||||
# Supportärenden: fritext som användaren SJÄLV skriver till supporten via
|
||||
@@ -634,9 +637,14 @@ processing_activities:
|
||||
stored_in:
|
||||
- posthog_eu # externt hos biträdet; inget lagras i vår databas
|
||||
security_measures:
|
||||
- session_replay_masks_all_text # maskTextSelector '*' utöver maskAllInputs
|
||||
- session_replay_masks_all_inputs
|
||||
- org_number_never_transmitted # låst av test i lib/analytics/__tests__
|
||||
# Mönsterbaserad maskering (2026-08-06, ersätter maskera-allt-läget):
|
||||
# belopp (valutaformaterad text), person-/organisationsnummer (text och
|
||||
# inmatning) och lösenord maskeras; övrig text och tangentbordsinmatning
|
||||
# syns i inspelningen för felsökning. data-ph-mask tvingar maskering av
|
||||
# utpekade PII-element; låst av test i lib/analytics/__tests__.
|
||||
- session_replay_masks_amounts_and_identity_numbers # lib/analytics/replay-masking.ts
|
||||
- session_replay_masks_password_inputs
|
||||
- org_number_never_transmitted_as_group_property # låst av test i lib/analytics/__tests__
|
||||
- no_pii_in_event_properties # PII endast som person properties via identify()
|
||||
# persistence: 'memory' -> ingen analysdata och inga kakor på enheten.
|
||||
# TVÅ undantag, båda funktionell UI-state utan personuppgifter, båda
|
||||
|
||||
@@ -799,5 +799,6 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-05] Dropped "ML 13 kap 8 §" cites for kontantmetoden VAT timing (comments/docs only): section is the old ML 1994:200 numbering; in ML 2023:200, 13 kap is input-VAT deduction. Rule stated without section cite until the current-law section is verified.
|
||||
[2026-08-05] The in-app assistant now reads invoice_inbox_items.channel_context (the answers a user gave in WhatsApp) as first-class underlag context, marked "uppgivna av användaren" and ranked above OCR output. Found in the field: the assistant asked for representation participants the user had typed into WhatsApp minutes earlier, because the intent's inbox query selected only document_id + extracted_data. Also backfilled by document_id, since a receipt can reach the intent through the document paths without the inbox row being matched to the transaction.
|
||||
[2026-08-05] The WhatsApp representation question now asks ONCE for a missing purpose instead of silently storing participants with purpose=null. Skatteverket wants participants AND purpose; accepting half and saying "Tack!" produced an undocumented deduction. Anti-loop: the follow-up fires only when no representation block exists yet, so a second incomplete answer is accepted as-is rather than nagging.
|
||||
[2026-08-06] Session replay masking narrowed from mask-everything to pattern-based (founder-approved): fully masked replays were wall-to-wall asterisks and useless for support debugging. lib/analytics/replay-masking.ts masks currency-shaped text, person-/organisationsnummer (text and typed input) and password inputs; data-ph-mask still force-masks tagged PII, data-ph-unmask stays honored for chrome, and everything else including typed input is now visible in replays. Privacy policy and RoPA updated in the same change; supersedes the 2026-07-27 maskTextSelector '*' decision.
|
||||
[2026-08-06] ROT/RUT payout strings were placed in the invoice_editor namespace while RotRutPayoutDialog and the invoices page read useTranslations('invoices'), so all 44 labels rendered as raw "invoices.rot_rut_*" key paths in production since #1380. Moved the keys to invoices rather than repointing the components, since the dialog belongs to the invoice list, not the editor. Message files are edited textually, never via JSON.parse/stringify: they contain duplicate keys a round trip would silently drop. Same bug class fixed in TemplateBookDialog (bookkeeping) and Correction/StrikeLines dialogs (journal_detail) by adding the strings to the namespace each component reads, matching the existing precedent that toast_posted_* is duplicated across journal_list and journal_detail. Added i18n/__tests__/message-keys.test.ts, which resolves every literal t() key against both locales: next-intl has no build-time check and fails by rendering the key path, so nothing caught this before users did.
|
||||
[2026-08-06] The ROT/RUT payout button is hidden from the invoices header unless the company has an invoice with deduction_total > 0 or rot_rut_enabled is on in tax settings. ROT/RUT concerns only companies selling eligible work to consumers, and a payout can never precede the invoice that created the claim, so the derived signal cannot hide the action from someone who needs it. Read from the company_settings row the page already fetches for ore_rounding (no extra round trip); deliberately not scoped to the fiscal-year filter, since a begäran is claimed the year after payment. ?rot-rut=1 still opens the dialog, so the feature is hidden, not removed.
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function PrivacyPolicyPage() {
|
||||
Integritetspolicy
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Senast uppdaterad: 2026-07-22
|
||||
Senast uppdaterad: 2026-08-06
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -157,10 +157,12 @@ export default function PrivacyPolicyPage() {
|
||||
enkäter och supportärenden. Överförda uppgifter:
|
||||
användar-ID, e-postadress, namn och företagsnamn. Om du
|
||||
själv skriver till supporten i appen skickas även ditt
|
||||
meddelande dit som ett ärende, så att vi kan svara. All text i
|
||||
sessionsinspelningar maskeras: vi spelar in var i
|
||||
gränssnittet du klickar, aldrig vad som står i din
|
||||
bokföring. Organisationsnummer överförs aldrig.
|
||||
meddelande dit som ett ärende, så att vi kan svara. I
|
||||
sessionsinspelningar maskeras belopp, person- och
|
||||
organisationsnummer samt lösenord; övrig text i
|
||||
gränssnittet och det du själv skriver kan ingå i
|
||||
inspelningen, så att vi kan felsöka problem du stöter
|
||||
på. Organisationsnummer skickas aldrig som analysdata.
|
||||
Identifiering sker endast för inloggade användare (ej
|
||||
sandbox/demo). Inga kakor används, och själva analysdatan
|
||||
lagras inte på din enhet. Två små tekniska värden sparas
|
||||
|
||||
@@ -14,10 +14,10 @@ const Label = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
// data-ph-unmask: form field labels are static i18n chrome, so session
|
||||
// replays show WHICH field is being filled in while the typed value stays
|
||||
// masked (see instrumentation-client.ts). A call site whose label text is
|
||||
// user data must add data-ph-mask, which wins over this default.
|
||||
// data-ph-unmask: form field labels are static i18n chrome, exempt from
|
||||
// the pattern-based replay masking (see lib/analytics/replay-masking.ts).
|
||||
// A call site whose label text is user data must add data-ph-mask, which
|
||||
// wins over this default.
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
data-ph-unmask=""
|
||||
|
||||
+21
-29
@@ -1,6 +1,7 @@
|
||||
import posthog from 'posthog-js'
|
||||
import { isAnalyticsEnabled, warnIfAnalyticsMisconfigured } from '@/lib/analytics/enabled'
|
||||
import { purgeLegacyAnalyticsStorage } from '@/lib/analytics/purge-legacy-storage'
|
||||
import { replayMaskInput, replayMaskText } from '@/lib/analytics/replay-masking'
|
||||
|
||||
// Clear anything Recapt left on the device. Runs unconditionally, BEFORE the
|
||||
// analytics gate: a browser carrying `__recapt_record_engine` must get cleaned
|
||||
@@ -53,31 +54,27 @@ function tracingHosts(): string[] {
|
||||
* `seenSurvey_*` flags straight to localStorage, bypassing this setting:
|
||||
* that is functional UI state ("don't ask again"), not tracking.
|
||||
*
|
||||
* 3. Mask-by-default text masking. `maskTextSelector: '*'` routes EVERY text
|
||||
* node through `maskTextFn`, which masks unless a `data-ph-unmask`
|
||||
* ancestor opts the node back in. This is an accounting app: org numbers
|
||||
* (which for a sole proprietorship ARE the owner's personal identity
|
||||
* number), customer names, balances and invoice amounts are rendered as
|
||||
* ordinary text, and PostHog masks inputs but NOT text by default.
|
||||
* Replays are for seeing WHERE a user gets stuck, never WHAT their books
|
||||
* say.
|
||||
* 3. Pattern-based replay masking (founder-approved 2026-08-06, supersedes
|
||||
* the 2026-07-27 mask-everything default that made replays wall-to-wall
|
||||
* asterisks). Replays exist so support can see WHERE a user gets stuck
|
||||
* and WHAT they typed while getting there; what stays unreadable is the
|
||||
* content of their books. `lib/analytics/replay-masking.ts` masks
|
||||
* currency-shaped text (every amount renders through `formatCurrency()`,
|
||||
* so one pattern covers all surfaces including future code), person-/
|
||||
* organisationsnummer (for an enskild firma the orgnr IS the owner's
|
||||
* personnummer) in both text and typed input, and password inputs.
|
||||
* Everything else, typed input included, is visible in the replay.
|
||||
*
|
||||
* `data-ph-unmask` is for static chrome only (nav labels, headings,
|
||||
* button text from i18n). User data nested inside an unmasked container
|
||||
* (active company name, user email, badge counts) gets `data-ph-mask`,
|
||||
* which wins because `closest()` finds the NEAREST tagged ancestor: when
|
||||
* both attributes land on the same element, mask still wins.
|
||||
* Anything untagged stays masked, so a forgotten tag fails safe.
|
||||
* `data-ph-mask` force-masks a subtree (deliberate PII spots: danger-zone
|
||||
* labels, user-defined dimension names, nav count bubbles) and
|
||||
* `data-ph-unmask` exempts one from pattern masking; the NEAREST tagged
|
||||
* ancestor wins, and mask wins when both land on the same element.
|
||||
*
|
||||
* The shared form label primitive (`components/ui/label.tsx`) carries
|
||||
* `data-ph-unmask` by default, so replays show WHICH field a user is
|
||||
* filling in while `maskAllInputs` keeps the typed value hidden. A call
|
||||
* site whose label text is user data (e.g. user-defined dimension names)
|
||||
* must add `data-ph-mask`.
|
||||
* rrweb only calls `maskInputFn` on inputs flagged by `maskInputOptions`,
|
||||
* so `maskAllInputs: true` stays set to flag every input and the function
|
||||
* decides per value. posthog-js force-merges `password: true` into
|
||||
* `maskInputOptions` on top of that.
|
||||
*/
|
||||
function maskText(text: string): string {
|
||||
return text.replace(/\S/g, '*')
|
||||
}
|
||||
if (warnIfAnalyticsMisconfigured() && isAnalyticsEnabled()) {
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, {
|
||||
api_host: '/rl',
|
||||
@@ -91,14 +88,9 @@ if (warnIfAnalyticsMisconfigured() && isAnalyticsEnabled()) {
|
||||
tracing_headers: tracingHosts(),
|
||||
session_recording: {
|
||||
maskAllInputs: true,
|
||||
maskInputFn: replayMaskInput,
|
||||
maskTextSelector: '*',
|
||||
maskTextFn: (text: string, element?: HTMLElement): string => {
|
||||
const tagged = element?.closest('[data-ph-unmask],[data-ph-mask]')
|
||||
if (!tagged || tagged.hasAttribute('data-ph-mask')) {
|
||||
return maskText(text)
|
||||
}
|
||||
return text
|
||||
},
|
||||
maskTextFn: replayMaskText,
|
||||
},
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { maskSensitiveText, replayMaskInput, replayMaskText } from '@/lib/analytics/replay-masking'
|
||||
|
||||
// Repo test convention. eventBus.clear() is deliberately absent: these are
|
||||
// pure functions and importing the bus would only add module side effects.
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
/**
|
||||
* Minimal stand-ins for the DOM elements rrweb hands to the masking
|
||||
* functions (tests run in the node environment, no jsdom).
|
||||
*/
|
||||
function fakeElement(opts: { type?: string; tagged?: 'mask' | 'unmask' | 'both' | null } = {}): HTMLElement {
|
||||
const attrs =
|
||||
opts.tagged === 'mask'
|
||||
? ['data-ph-mask']
|
||||
: opts.tagged === 'unmask'
|
||||
? ['data-ph-unmask']
|
||||
: opts.tagged === 'both'
|
||||
? ['data-ph-mask', 'data-ph-unmask']
|
||||
: []
|
||||
const tagged = attrs.length > 0 ? { hasAttribute: (name: string) => attrs.includes(name) } : null
|
||||
return {
|
||||
type: opts.type,
|
||||
closest: (_selector: string) => tagged,
|
||||
} as unknown as HTMLElement
|
||||
}
|
||||
|
||||
describe('maskSensitiveText', () => {
|
||||
it('masks sv-SE formatted amounts, preserving length and whitespace', () => {
|
||||
// First variant groups thousands with U+00A0 (what Intl sv-SE emits), the second with a regular space.
|
||||
expect(maskSensitiveText('1 234,56 kr')).toBe('* ****** **')
|
||||
expect(maskSensitiveText('1 234,56 kr')).toBe('* ****** **')
|
||||
})
|
||||
|
||||
it('masks negative amounts with both hyphen and the Intl minus sign', () => {
|
||||
expect(maskSensitiveText('-500 kr')).toBe('**** **')
|
||||
expect(maskSensitiveText('−1 234 kr')).toBe('** *** **')
|
||||
})
|
||||
|
||||
it('masks the amount inside surrounding text', () => {
|
||||
expect(maskSensitiveText('Totalt 1 234 kr att betala')).toBe('Totalt * *** ** att betala')
|
||||
expect(maskSensitiveText('999 kr/mån')).toBe('*** **/mån')
|
||||
})
|
||||
|
||||
it('masks other currency markers', () => {
|
||||
expect(maskSensitiveText('12,00 €')).toBe('***** *')
|
||||
expect(maskSensitiveText('10 US$')).toBe('** ***')
|
||||
expect(maskSensitiveText('1 000 SEK')).toBe('* *** ***')
|
||||
})
|
||||
|
||||
it('masks person- and organisationsnummer', () => {
|
||||
expect(maskSensitiveText('556677-8899')).toBe('***********')
|
||||
expect(maskSensitiveText('19850101-1234')).toBe('*************')
|
||||
expect(maskSensitiveText('850101+1234')).toBe('***********')
|
||||
})
|
||||
|
||||
it('leaves non-amount, non-identity text untouched', () => {
|
||||
for (const text of [
|
||||
'2026-08-06',
|
||||
'Verifikat A-217',
|
||||
'070-123 45 67',
|
||||
'5050-1055',
|
||||
'namn@exempel.se',
|
||||
'10 kronor',
|
||||
'E-postadress',
|
||||
'Konto 1930',
|
||||
]) {
|
||||
expect(maskSensitiveText(text)).toBe(text)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('replayMaskText', () => {
|
||||
it('pattern-masks when the node has no tagged ancestor', () => {
|
||||
expect(replayMaskText('Saldo 1 234 kr', fakeElement())).toBe('Saldo * *** **')
|
||||
expect(replayMaskText('Saldo 1 234 kr', undefined)).toBe('Saldo * *** **')
|
||||
})
|
||||
|
||||
it('masks everything under data-ph-mask', () => {
|
||||
expect(replayMaskText('Acme AB', fakeElement({ tagged: 'mask' }))).toBe('**** **')
|
||||
})
|
||||
|
||||
it('passes everything through under data-ph-unmask', () => {
|
||||
expect(replayMaskText('Belopp i kr', fakeElement({ tagged: 'unmask' }))).toBe('Belopp i kr')
|
||||
})
|
||||
|
||||
it('lets mask win when both attributes land on the same element', () => {
|
||||
expect(replayMaskText('Acme AB', fakeElement({ tagged: 'both' }))).toBe('**** **')
|
||||
})
|
||||
})
|
||||
|
||||
describe('replayMaskInput', () => {
|
||||
it('always masks password inputs, even under data-ph-unmask', () => {
|
||||
expect(replayMaskInput('hunter2', fakeElement({ type: 'password' }))).toBe('*******')
|
||||
expect(replayMaskInput('hunter2', fakeElement({ type: 'password', tagged: 'unmask' }))).toBe('*******')
|
||||
})
|
||||
|
||||
it('masks identity-number-shaped values, including partial typing', () => {
|
||||
expect(replayMaskInput('556677-8899', fakeElement({ type: 'text' }))).toBe('***********')
|
||||
expect(replayMaskInput('19850101-1234', fakeElement({ type: 'text' }))).toBe('*************')
|
||||
expect(replayMaskInput('5566778', fakeElement({ type: 'text' }))).toBe('*******')
|
||||
})
|
||||
|
||||
it('passes ordinary typed values through', () => {
|
||||
for (const value of ['1234,56', 'Kaffe till kontoret', 'namn@exempel.se', '1930', 'Acme AB']) {
|
||||
expect(replayMaskInput(value, fakeElement({ type: 'text' }))).toBe(value)
|
||||
}
|
||||
})
|
||||
|
||||
it('honors data-ph-mask on inputs', () => {
|
||||
expect(replayMaskInput('Acme AB', fakeElement({ type: 'text', tagged: 'mask' }))).toBe('**** **')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Pattern-based masking for PostHog session replay.
|
||||
*
|
||||
* Replays are visible by default so support can see WHERE a user gets stuck
|
||||
* and WHAT they typed while getting there. What must never be readable in a
|
||||
* replay is the content of a user's books and identity numbers:
|
||||
*
|
||||
* 1. Monetary amounts. Every amount in the app renders through
|
||||
* `formatCurrency()` (Intl sv-SE currency style, e.g. "1 234,56 kr"), so a
|
||||
* currency-shaped text pattern covers transactions, vouchers, reports,
|
||||
* invoices and dashboards in one place, including future code, without
|
||||
* tagging hundreds of render sites.
|
||||
* 2. Person- and organisationsnummer. For an enskild firma the orgnr IS the
|
||||
* owner's personnummer. Masked both as rendered text (formatOrgNumber()
|
||||
* output, "556677-8899") and as typed input values.
|
||||
* 3. Passwords. Always masked, never overridable.
|
||||
*
|
||||
* Tag overrides (nearest tagged ancestor wins, mask wins on a tie):
|
||||
* - `data-ph-mask` force-masks an element's whole subtree (used on deliberate
|
||||
* PII spots: company name / email in danger-zone labels, user-defined
|
||||
* dimension names, nav count bubbles).
|
||||
* - `data-ph-unmask` exempts a subtree from pattern masking (static chrome
|
||||
* such as form labels and nav). It never unmasks a password input.
|
||||
*
|
||||
* Known limits, accepted deliberately: masking is length-preserving (star
|
||||
* count reveals magnitude, layout stays stable in the replay), bare numbers
|
||||
* without a currency marker stay visible, and an identity number rendered
|
||||
* WITHOUT its separator is only caught on the input side.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Currency-shaped text: optional sign (Intl sv-SE renders negative amounts
|
||||
* with U+2212, hand-written strings use '-'), digits with space/nbsp grouping
|
||||
* and a decimal part, then a currency marker. The trailing lookahead rejects
|
||||
* letter continuations so "10 kronor" or "SEKTION" never match.
|
||||
*/
|
||||
const AMOUNT_PATTERN = new RegExp(
|
||||
// − is the Unicode minus sign Intl sv-SE emits for negative amounts.
|
||||
String.raw`[-−]?\d(?:[\d\s]|[.,](?=\d))*\s?(?:kr|sek|eur|usd|nok|dkk|gbp|chf|us\$|\$|€|£)(?![\p{L}\d])`,
|
||||
'giu',
|
||||
)
|
||||
|
||||
/**
|
||||
* Person-/organisationsnummer rendered as text: 6 or 8 digits, separator,
|
||||
* 4 digits ("556677-8899", "19850101-1234", "850101+1234"). The digit
|
||||
* lookarounds keep bankgiro ("5050-1055"), phone numbers and dates out.
|
||||
*/
|
||||
const IDENTITY_TEXT_PATTERN = /(?<!\d)\d{6}(?:\d{2})?[-+]\d{4}(?!\d)/g
|
||||
|
||||
/**
|
||||
* A typed input value that is (or is on its way to becoming) a person-/
|
||||
* organisationsnummer: 6 or more leading digits, optionally a separator and
|
||||
* up to 4 more. Matching from the 6th digit means intermediate keystroke
|
||||
* snapshots never ship the birthdate prefix of a personnummer. Accepted
|
||||
* over-masking: any bare 6-12 digit value (e.g. a raw amount over 99 999)
|
||||
* is masked too; amounts with decimals or thousand separators stay visible.
|
||||
*/
|
||||
const IDENTITY_INPUT_PATTERN = /^\s*\d{6,8}[-+ ]?\d{0,4}\s*$/
|
||||
|
||||
const TAG_SELECTOR = '[data-ph-mask],[data-ph-unmask]'
|
||||
|
||||
/** Length-preserving mask: whitespace survives so table layout stays legible. */
|
||||
function maskAll(text: string): string {
|
||||
return text.replace(/\S/g, '*')
|
||||
}
|
||||
|
||||
function maskSpan(span: string): string {
|
||||
return maskAll(span)
|
||||
}
|
||||
|
||||
/**
|
||||
* Masks currency amounts and separator-formatted identity numbers inside a
|
||||
* text node, leaving the surrounding text readable.
|
||||
*/
|
||||
export function maskSensitiveText(text: string): string {
|
||||
return text.replace(AMOUNT_PATTERN, maskSpan).replace(IDENTITY_TEXT_PATTERN, maskSpan)
|
||||
}
|
||||
|
||||
/**
|
||||
* `session_recording.maskTextFn`. Runs on EVERY text node because
|
||||
* `maskTextSelector: '*'` flags them all; this function then decides.
|
||||
*/
|
||||
export function replayMaskText(text: string, element?: HTMLElement): string {
|
||||
const tagged = element?.closest(TAG_SELECTOR)
|
||||
if (tagged) {
|
||||
return tagged.hasAttribute('data-ph-mask') ? maskAll(text) : text
|
||||
}
|
||||
return maskSensitiveText(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* `session_recording.maskInputFn`. rrweb only invokes this on inputs flagged
|
||||
* by `maskInputOptions`, so the config sets `maskAllInputs: true` to flag
|
||||
* every input and this function selectively passes values through. Password
|
||||
* checks come first: not even `data-ph-unmask` may reveal one.
|
||||
*/
|
||||
export function replayMaskInput(text: string, element?: HTMLElement): string {
|
||||
if ((element as HTMLInputElement | undefined)?.type === 'password') {
|
||||
return maskAll(text)
|
||||
}
|
||||
const tagged = element?.closest?.(TAG_SELECTOR)
|
||||
if (tagged) {
|
||||
return tagged.hasAttribute('data-ph-mask') ? maskAll(text) : text
|
||||
}
|
||||
if (IDENTITY_INPUT_PATTERN.test(text)) {
|
||||
return maskAll(text)
|
||||
}
|
||||
return text
|
||||
}
|
||||
Reference in New Issue
Block a user