@angular-helpers/security 21.2.0 → 21.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ import { inject, resource } from '@angular/core';
2
+ import { validate, validateAsync } from '@angular/forms/signals';
3
+ import { assessPasswordStrength, isHtmlSafe, isUrlSafe, containsScriptInjection, containsSqlInjectionHints, HibpService } from '@angular-helpers/security';
4
+
5
+ /**
6
+ * Registers a sync validation rule on a string field that fails when the password strength
7
+ * is below the required score. Uses the shared `assessPasswordStrength` helper for behavioural
8
+ * parity with the Reactive Forms `SecurityValidators.strongPassword`.
9
+ *
10
+ * @example
11
+ * form(model, (p) => {
12
+ * required(p.password);
13
+ * strongPassword(p.password, { minScore: 3 });
14
+ * });
15
+ */
16
+ function strongPassword(path, options) {
17
+ const required = options?.minScore ?? 2;
18
+ const message = options?.message ?? 'Password is too weak';
19
+ validate(path, ({ value }) => {
20
+ const raw = value();
21
+ if (!raw)
22
+ return null;
23
+ const { score } = assessPasswordStrength(raw);
24
+ return score < required ? { kind: 'weakPassword', message } : null;
25
+ });
26
+ }
27
+ /**
28
+ * Registers a sync validation rule that fails when the input contains tags or attributes
29
+ * outside the allowed HTML sanitizer allowlist.
30
+ *
31
+ * Requires a browser environment. In SSR contexts the rule is a no-op (returns `null`) so
32
+ * server-rendered forms remain submittable; re-validation happens on hydration.
33
+ */
34
+ function safeHtml(path, options) {
35
+ const message = options?.message ?? 'Value contains unsafe HTML';
36
+ const sanitizerOptions = {
37
+ allowedTags: options?.allowedTags,
38
+ allowedAttributes: options?.allowedAttributes,
39
+ };
40
+ validate(path, ({ value }) => {
41
+ const raw = value();
42
+ if (!raw)
43
+ return null;
44
+ if (typeof DOMParser === 'undefined')
45
+ return null;
46
+ return isHtmlSafe(raw, sanitizerOptions) ? null : { kind: 'unsafeHtml', message };
47
+ });
48
+ }
49
+ /**
50
+ * Registers a sync validation rule that fails for malformed URLs or URLs using schemes
51
+ * outside the allowlist (default: `http:` and `https:`).
52
+ */
53
+ function safeUrl(path, options) {
54
+ const schemes = options?.schemes ?? ['http:', 'https:'];
55
+ const message = options?.message ?? 'URL scheme is not allowed';
56
+ validate(path, ({ value }) => {
57
+ const raw = value();
58
+ if (!raw)
59
+ return null;
60
+ return isUrlSafe(raw, schemes) ? null : { kind: 'unsafeUrl', message };
61
+ });
62
+ }
63
+ /**
64
+ * Registers a sync validation rule that rejects values matching common script-injection
65
+ * sentinels (`<script>`, `javascript:`, inline event handlers).
66
+ */
67
+ function noScriptInjection(path, options) {
68
+ const message = options?.message ?? 'Value contains script injection patterns';
69
+ validate(path, ({ value }) => {
70
+ const raw = value();
71
+ if (!raw)
72
+ return null;
73
+ return containsScriptInjection(raw) ? { kind: 'scriptInjection', message } : null;
74
+ });
75
+ }
76
+ /**
77
+ * Registers a sync validation rule that rejects common SQL-injection sentinel strings.
78
+ * Intended as defense-in-depth alongside server-side parameterized queries.
79
+ */
80
+ function noSqlInjectionHints(path, options) {
81
+ const message = options?.message ?? 'Value contains SQL injection hints';
82
+ validate(path, ({ value }) => {
83
+ const raw = value();
84
+ if (!raw)
85
+ return null;
86
+ return containsSqlInjectionHints(raw) ? { kind: 'sqlInjectionHint', message } : null;
87
+ });
88
+ }
89
+ /**
90
+ * Registers an async validation rule that checks the password against the Have I Been Pwned
91
+ * breach corpus via the k-anonymity API. Requires `provideHibp()` to be set up in the
92
+ * injector hierarchy.
93
+ *
94
+ * Fail-open semantics: network errors and unsupported environments do NOT produce a
95
+ * validation error — the form remains submittable. This is intentional to prevent HIBP
96
+ * outages from blocking user sign-ups.
97
+ *
98
+ * @example
99
+ * form(model, (p) => {
100
+ * required(p.password);
101
+ * strongPassword(p.password, { minScore: 3 });
102
+ * hibpPassword(p.password);
103
+ * });
104
+ */
105
+ function hibpPassword(path, options) {
106
+ const message = options?.message ?? 'This password has appeared in a data breach';
107
+ const debounceMs = options?.debounceMs ?? 300;
108
+ validateAsync(path, {
109
+ params: ({ value }) => {
110
+ const raw = value();
111
+ return raw && raw.length >= 8 ? raw : undefined;
112
+ },
113
+ factory: (passwordSignal) => {
114
+ const hibp = inject(HibpService);
115
+ return resource({
116
+ params: () => passwordSignal(),
117
+ loader: async ({ params: password, abortSignal }) => {
118
+ if (!password)
119
+ return undefined;
120
+ if (debounceMs > 0) {
121
+ await debounce(debounceMs, abortSignal);
122
+ }
123
+ return hibp.isPasswordLeaked(password);
124
+ },
125
+ });
126
+ },
127
+ onSuccess: (result) => {
128
+ if (!result)
129
+ return null;
130
+ if (result.error)
131
+ return null;
132
+ return result.leaked ? { kind: 'leakedPassword', message, count: result.count } : null;
133
+ },
134
+ onError: () => null,
135
+ });
136
+ }
137
+ function debounce(ms, signal) {
138
+ return new Promise((resolve, reject) => {
139
+ if (signal.aborted) {
140
+ reject(signal.reason);
141
+ return;
142
+ }
143
+ const timer = setTimeout(() => {
144
+ signal.removeEventListener('abort', onAbort);
145
+ resolve();
146
+ }, ms);
147
+ const onAbort = () => {
148
+ clearTimeout(timer);
149
+ reject(signal.reason);
150
+ };
151
+ signal.addEventListener('abort', onAbort, { once: true });
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Generated bundle index. Do not edit.
157
+ */
158
+
159
+ export { hibpPassword, noScriptInjection, noSqlInjectionHints, safeHtml, safeUrl, strongPassword };