@zerotal/arch 1.7.0

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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,380 @@
1
+ ---
2
+ title: Encryption & Hashing
3
+ description: Encrypt values symmetrically with APP_KEY, hash passwords one-way, and sign tamper-proof URLs.
4
+ ---
5
+
6
+ # Encryption & Hashing
7
+
8
+ Three security primitives cover the ways an application protects a value, and
9
+ which one you want follows from a single question: do you need to read the value
10
+ back?
11
+
12
+ | Reach for | When | Reversible |
13
+ | ----------- | ---------------------------------------- | -------------------------------- |
14
+ | **`Crypt`** | Cookies, stored secrets, signed payloads | Yes — you decrypt it |
15
+ | **`Hash`** | Passwords | No — you verify a guess |
16
+ | **`Url`** | Verification, invite and one-time links | Not encrypted, only tamper-proof |
17
+
18
+ A password is never encrypted, only hashed: there is no legitimate reason to
19
+ recover the original, and a store you _can_ decrypt is a store an attacker can
20
+ decrypt too. A signed URL is the opposite case — its payload stays readable in the
21
+ query string, and the signature only proves nobody altered it.
22
+
23
+ All three are keyed by `APP_KEY`, which `bun create zerotal` generates. Rotate it
24
+ with `bun zt key:generate`.
25
+
26
+ ## Getting Started
27
+
28
+ `Crypt` and the core `Hash` primitive live in `zerotal/security`, with
29
+ nothing to install and no provider to register:
30
+
31
+ ```typescript
32
+ import { Crypt, Hash } from "zerotal/security";
33
+ ```
34
+
35
+ `Url` is a core service exported from `zerotal/http`:
36
+
37
+ ```typescript
38
+ import { Url } from "zerotal/http";
39
+ ```
40
+
41
+ > **Note** — Two different `Hash` symbols ship, and they are not interchangeable.
42
+ > `zerotal/security` exports the zero-config primitive documented here,
43
+ > usable anywhere including in a test that never boots the app.
44
+ > `@zerotal/auth` exports a facade of the same name that resolves the `hash`
45
+ > container binding, reads its algorithm from `config/auth.ts`, and is therefore
46
+ > only usable **after `app.boot()`**. Application code that already depends on
47
+ > auth should import from `@zerotal/auth` so the configured algorithm applies;
48
+ > anything lower-level should use the core primitive.
49
+
50
+ ## Crypt — symmetric encryption
51
+
52
+ `Crypt` uses **AES-256-GCM** (authenticated encryption) keyed by `APP_KEY`.
53
+ Tampered or truncated payloads fail with a `DecryptionError` instead of returning
54
+ garbage. Generate a key with `zerotal key:generate` (sets `APP_KEY`).
55
+
56
+ ```ts
57
+ // in a controller or service
58
+ import { Crypt } from "zerotal/security";
59
+
60
+ const token = Crypt.encryptString("secret"); // opaque base64 (iv + tag + ciphertext)
61
+ Crypt.decryptString(token); // 'secret'
62
+
63
+ const blob = Crypt.encrypt({ userId: 7 }); // any JSON-serializable value
64
+ Crypt.decrypt<{ userId: number }>(blob).userId; // 7
65
+ ```
66
+
67
+ - `Crypt.setKey(key)` overrides the `APP_KEY`-derived key (accepts raw or
68
+ `base64:`-prefixed; any length — it's hashed to 32 bytes via SHA-256).
69
+ - Without a key, encryption throws `CryptKeyMissingError`.
70
+ - A new IV is generated per call, so encrypting the same value twice yields
71
+ different ciphertexts.
72
+
73
+ > **Danger** — `APP_KEY` is the root of all `Crypt` security. Rotating it makes
74
+ > every existing ciphertext undecryptable, and leaking it exposes every encrypted
75
+ > payload. Keep it out of version control and set it per environment — see
76
+ > [Config system](/docs/config-system).
77
+
78
+ ## Hash — password hashing
79
+
80
+ `Hash` wraps `Bun.password` — **argon2id** by default, bcrypt optional. The
81
+ algorithm is auto-detected on verify, so changing the default never breaks
82
+ existing hashes.
83
+
84
+ ```ts
85
+ // in a controller or service
86
+ import { Hash } from "zerotal/security";
87
+
88
+ const hash = await Hash.make("secret123"); // argon2id
89
+ const valid = await Hash.verify("secret123", hash); // true (alias: Hash.check)
90
+
91
+ await Hash.make("secret123", { algorithm: "bcrypt" }); // opt into bcrypt
92
+ Hash.setDefault("bcrypt"); // change the default
93
+ Hash.needsRehash(oldHash); // true if not the default algorithm
94
+ ```
95
+
96
+ `verify` returns `false` (never throws) for a malformed or non-matching hash.
97
+ Never store or compare plain-text passwords — `make` always returns a new,
98
+ salted hash, so two calls on the same password produce different strings.
99
+
100
+ > **Tip** — Call `Hash.needsRehash(hash)` right after a successful `verify` at
101
+ > login. If it returns `true`, re-hash the plaintext the user just supplied and
102
+ > store the new hash to transparently migrate them to the current algorithm.
103
+
104
+ ### Which algorithm should I use?
105
+
106
+ `Hash.make` accepts `argon2id`, `argon2i`, `argon2d`, or `bcrypt`.
107
+
108
+ - **`argon2id` (default)** — the recommended choice for new applications; resists
109
+ both GPU and side-channel attacks. Keep it unless you have a specific reason not to.
110
+ - **`bcrypt`** — choose it for interop with an existing bcrypt-based password
111
+ store, or to match another system's hashes.
112
+ - **`argon2i` / `argon2d`** — niche variants; prefer `argon2id` unless a threat
113
+ model specifically calls for one.
114
+
115
+ Both algorithms verify correctly against their own stored hashes regardless of the
116
+ configured default, because the algorithm is encoded in the hash string itself.
117
+ That is what makes switching safe, and what makes a migration a matter of
118
+ rehashing on login rather than a mass update.
119
+
120
+ > **Danger** — bcrypt truncates at 72 bytes. If you must use bcrypt, it silently
121
+ > ignores input past 72 bytes, weakening long passwords — a non-issue for
122
+ > Argon2id, which is the default.
123
+
124
+ When authentication is in play the algorithm comes from `config/auth.ts` instead
125
+ of `setDefault`:
126
+
127
+ ```ts
128
+ // config/auth.ts
129
+ import { AuthConfig } from "@zerotal/auth";
130
+
131
+ export default AuthConfig({
132
+ algorithm: "argon2id", // 'argon2id' (default) | 'bcrypt'
133
+ });
134
+ ```
135
+
136
+ ## Signed URLs
137
+
138
+ The `Url` facade generates and verifies HMAC-SHA256 signed URLs. Use it for:
139
+
140
+ - Email verification links
141
+ - Password reset links (if you roll your own instead of `PasswordBroker`)
142
+ - One-time invite links
143
+ - Any time-limited, tamper-proof URL
144
+
145
+ `Url.sign` / `Url.verify` sign with `APP_KEY` automatically, so you never have to
146
+ thread a signing key through your code.
147
+
148
+ ### Generating a signed URL
149
+
150
+ ```ts
151
+ function sign(
152
+ base: string,
153
+ params?: Record<string, string>,
154
+ expiresInMinutes?: number,
155
+ secret?: string,
156
+ ): string;
157
+ ```
158
+
159
+ ```ts
160
+ // in a controller
161
+ import { Url } from "zerotal/http";
162
+
163
+ const url = Url.sign(
164
+ "https://myapp.com/auth/verify",
165
+ { email: "user@example.com", id: "42" }, // extra query params
166
+ 60, // expires in 60 minutes (default)
167
+ );
168
+ // https://myapp.com/auth/verify?email=user%40example.com&id=42&expires=1750000000&signature=abc123…
169
+ ```
170
+
171
+ Pass a fourth argument to sign with a key other than `APP_KEY` (e.g. a
172
+ per-feature invite secret): `Url.sign(base, params, minutes, inviteSecret)`.
173
+
174
+ ### Verifying a signed URL
175
+
176
+ ```ts
177
+ // in a controller
178
+ import { Url } from "zerotal/http";
179
+
180
+ const ok = Url.verify(url);
181
+ // false if tampered, expired, or missing signature/expires params
182
+
183
+ // Verify against an explicit secret instead of APP_KEY:
184
+ Url.verify(url, inviteSecret);
185
+ ```
186
+
187
+ `Url.verify()` never throws — a malformed URL, bad signature, or expired link all
188
+ return `false`, so you can branch on the boolean directly.
189
+
190
+ ### How the signature works
191
+
192
+ - The signature is `HMAC-SHA256(secret, payload)` where the payload is the full
193
+ URL (minus the `signature` param) with its query parameters **sorted by key** —
194
+ so the order of params in the link doesn't affect verification.
195
+ - `expires` is a Unix timestamp **in seconds**; `verify()` checks it before doing
196
+ any crypto, so expired links are rejected cheaply.
197
+ - Comparison is **constant-time**, preventing timing attacks against the
198
+ signature.
199
+ - `Url.sign` throws `UrlKeyMissingError` if no `APP_KEY` is set and no explicit
200
+ secret is passed — links are never signed with a blank or guessable key.
201
+
202
+ > **Danger** — Generate a strong `APP_KEY` with `zerotal key:generate` before
203
+ > signing anything in production. A weak or shared key lets anyone forge valid
204
+ > links.
205
+
206
+ ### ValidateSignatureMiddleware
207
+
208
+ Apply `ValidateSignatureMiddleware` to any route that receives signed URLs — it
209
+ rejects invalid or expired links with a 403 JSON response, so your controller
210
+ only runs when the signature is good:
211
+
212
+ ```ts
213
+ // routes/web.ts
214
+ import { ValidateSignatureMiddleware } from "@zerotal/auth";
215
+
216
+ // Reads the signing secret from config('app.key') (sourced from APP_KEY)
217
+ Router.get("/auth/verify", VerificationController, "verify", [ValidateSignatureMiddleware]);
218
+
219
+ // Override the secret for a specific route
220
+ Router.get("/invite/:token", InviteController, "accept", [
221
+ ValidateSignatureMiddleware.with({ secret: env("INVITE_SECRET", "") }),
222
+ ]);
223
+ ```
224
+
225
+ > **Warning** — If neither `config('app.key')` nor an explicit `secret` resolves,
226
+ > the middleware throws a `ConfigError` rather than silently accepting forged
227
+ > links. Set `APP_KEY` or pass `.with({ secret })`.
228
+
229
+ Override `onInvalid()` to return a custom `Response` instead of the default 403
230
+ JSON:
231
+
232
+ ```ts
233
+ // app/middleware/HtmlSignatureMiddleware.ts
234
+ import { ValidateSignatureMiddleware } from "@zerotal/auth";
235
+ import type { HttpContext } from "zerotal";
236
+
237
+ class HtmlSignatureMiddleware extends ValidateSignatureMiddleware {
238
+ protected onInvalid(_ctx: HttpContext): Response {
239
+ return new Response("This link is invalid or has expired.", {
240
+ status: 403,
241
+ headers: { "Content-Type": "text/html" },
242
+ });
243
+ }
244
+ }
245
+ ```
246
+
247
+ ## Testing
248
+
249
+ Set your suite up once as described in [Testing](/docs/testing). `Crypt` needs an
250
+ `APP_KEY` and nothing else, so its tests are ordinary function tests.
251
+
252
+ **Assert the round trip, not the ciphertext.** Encryption is randomised — the
253
+ same input produces a different payload every time — so a test pinned to a
254
+ literal string fails on the next run:
255
+
256
+ ```typescript
257
+ // tests/security/Crypt.test.ts
258
+ import { test, expect } from "bun:test";
259
+ import { Crypt } from "zerotal/security";
260
+
261
+ test("a value survives the round trip", () => {
262
+ const token = Crypt.encryptString("card-4242");
263
+
264
+ expect(token).not.toBe("card-4242"); // it is actually encrypted
265
+ expect(Crypt.decryptString(token)).toBe("card-4242");
266
+ });
267
+
268
+ test("encrypting twice produces different payloads", () => {
269
+ expect(Crypt.encryptString("same")).not.toBe(Crypt.encryptString("same"));
270
+ });
271
+ ```
272
+
273
+ That second test is the one worth having. If it ever fails, the cipher has lost
274
+ its randomness and identical plaintexts have become linkable — a real weakness
275
+ that a round-trip test alone would never notice.
276
+
277
+ **Tampering must throw**, and proving it is what tells you the payload is
278
+ authenticated rather than merely scrambled:
279
+
280
+ ```typescript
281
+ // tests/security/Crypt.test.ts
282
+ import { DecryptionError } from "zerotal/security";
283
+
284
+ test("a modified payload is rejected", () => {
285
+ const token = Crypt.encryptString("card-4242");
286
+ const tampered = token.slice(0, -2) + (token.endsWith("A") ? "B" : "A");
287
+
288
+ expect(() => Crypt.decryptString(tampered)).toThrow(DecryptionError);
289
+ });
290
+ ```
291
+
292
+ **`encrypt()` round-trips any JSON value**, so a test covering an object confirms
293
+ the serialisation as well as the cipher:
294
+
295
+ ```typescript
296
+ // tests/security/Crypt.test.ts
297
+ const payload = Crypt.encrypt({ userId: 7, scopes: ["read"] });
298
+
299
+ expect(Crypt.decrypt<{ userId: number }>(payload).userId).toBe(7);
300
+ ```
301
+
302
+ Hashing has its own trap: every `Hash` method except `needsRehash` is async, and a
303
+ forgotten `await` yields a `Promise`, which is always truthy — the one mistake in
304
+ this API that silently passes.
305
+
306
+ ```typescript
307
+ // tests/security/Hash.test.ts
308
+ import { Hash } from "zerotal/security";
309
+
310
+ test("a password verifies against its own hash", async () => {
311
+ const hash = await Hash.make("correct-horse");
312
+
313
+ expect(await Hash.check("correct-horse", hash)).toBe(true);
314
+ expect(await Hash.check("wrong-horse", hash)).toBe(false);
315
+ });
316
+
317
+ test("the same password hashes differently each time", async () => {
318
+ expect(await Hash.make("same")).not.toBe(await Hash.make("same"));
319
+ });
320
+ ```
321
+
322
+ **Assert the false case.** `expect(await Hash.check(...)).toBe(true)` passes
323
+ against a verifier that returns `true` for everything; the wrong-password
324
+ assertion is what proves it actually checks. And because hashes are salted, never
325
+ compare two of them — the same password hashes differently every time, which is
326
+ precisely the property that makes a stolen table unusable.
327
+
328
+ > **Warning** — Hashing is deliberately slow. A test that hashes in a loop, or a
329
+ > factory that hashes a password for every seeded user, will dominate your suite's
330
+ > runtime. Hash once and reuse the result, or seed users with a pre-computed hash.
331
+
332
+ One piece of environment setup applies to all of the above:
333
+
334
+ > **Warning** — Give the test suite its own `APP_KEY`. Sharing the production key
335
+ > with a test environment means a leaked test fixture decrypts real data, and
336
+ > rotating the key breaks the suite for reasons nobody will connect to the change.
337
+
338
+ ## References
339
+
340
+ ### `Crypt`
341
+
342
+ | Method | Signature | Description |
343
+ | --------------- | ----------------------------------- | --------------------------------------------------------------------------- |
344
+ | `encryptString` | `(plain: string): string` | Encrypt a UTF-8 string to an opaque base64 payload (iv + tag + ciphertext). |
345
+ | `decryptString` | `(payload: string): string` | Decrypt an `encryptString` payload; throws `DecryptionError` on failure. |
346
+ | `encrypt` | `(value: unknown): string` | JSON-serialize then encrypt any value. |
347
+ | `decrypt` | `<T = unknown>(payload: string): T` | Decrypt and JSON-parse a value produced by `encrypt`. |
348
+ | `setKey` | `(key: string): void` | Override the `APP_KEY`-derived key (raw or `base64:` prefixed). |
349
+
350
+ `CryptKeyMissingError` and `DecryptionError` are exported from `zerotal/security`.
351
+
352
+ ### `Hash`
353
+
354
+ | Method | Signature | Description |
355
+ | ------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------ |
356
+ | `make` | `(value: string, options?: { algorithm?: HashAlgorithm }): Promise<string>` | Hash a value; defaults to the current default algorithm. |
357
+ | `verify` | `(value: string, hash: string): Promise<boolean>` | Verify a value against a stored hash; returns `false` on mismatch. |
358
+ | `check` | `(value: string, hash: string): Promise<boolean>` | Alias of `verify`. |
359
+ | `setDefault` | `(algorithm: HashAlgorithm): void` | Set the default hashing algorithm. |
360
+ | `needsRehash` | `(hash: string): boolean` | `true` when the hash was made with a non-default algorithm. |
361
+
362
+ `HashAlgorithm` is `"argon2id" | "argon2i" | "argon2d" | "bcrypt"`.
363
+
364
+ The `@zerotal/auth` facade of the same name adds `selfTest()` — hashes and
365
+ verifies a sentinel value, for health checks — and resolves from the container, so
366
+ it works only after the app has booted. In a unit test that never boots, construct
367
+ `new HashService("argon2id")` directly or use the core primitive above.
368
+
369
+ ### `Url`
370
+
371
+ | Method | Signature | Description |
372
+ | ---------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------- |
373
+ | `Url.sign(base, params?, min?, secret?)` | `(base: string, params?, expiresInMinutes?, secret?) => string` | Build an HMAC-signed, time-limited URL. |
374
+ | `Url.verify(url, secret?)` | `(signedUrl: string, secret?: string) => boolean` | Verify a signed URL; `false` if tampered or expired. |
375
+
376
+ ## Next steps
377
+
378
+ - [Authentication](/docs/authentication) — where hashed passwords are verified at login.
379
+ - [Password Reset](/docs/password-reset) — the `Url` facade and `PasswordBroker` in a full flow.
380
+ - [Config system](/docs/config-system) — manage `APP_KEY` and other secrets per environment.