pipework 0.61.0 → 0.63.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.
- package/CHANGELOG.md +16 -0
- package/REFERENCE.md +53 -0
- package/dist/REFERENCE.md +67 -3
- package/dist/auth/auth/index.d.ts +4 -0
- package/dist/auth/auth/index.d.ts.map +1 -1
- package/dist/auth/auth/index.js +2 -0
- package/dist/auth/auth/index.js.map +1 -1
- package/dist/auth/auth/namespace.d.ts +6 -0
- package/dist/auth/auth/namespace.d.ts.map +1 -1
- package/dist/auth/auth/namespace.js +6 -0
- package/dist/auth/auth/namespace.js.map +1 -1
- package/dist/auth/auth/passwords.d.ts +39 -0
- package/dist/auth/auth/passwords.d.ts.map +1 -0
- package/dist/auth/auth/passwords.js +76 -0
- package/dist/auth/auth/passwords.js.map +1 -0
- package/dist/auth/auth/schema.d.ts +27 -0
- package/dist/auth/auth/schema.d.ts.map +1 -1
- package/dist/auth/auth/schema.js +10 -1
- package/dist/auth/auth/schema.js.map +1 -1
- package/dist/auth/auth/sign-in.d.ts +64 -0
- package/dist/auth/auth/sign-in.d.ts.map +1 -0
- package/dist/auth/auth/sign-in.js +135 -0
- package/dist/auth/auth/sign-in.js.map +1 -0
- package/dist/core/config/namespace.d.ts +2 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +2 -0
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +4 -0
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/errors/http.d.ts +6 -0
- package/dist/core/errors/http.d.ts.map +1 -1
- package/dist/core/errors/http.js +9 -0
- package/dist/core/errors/http.js.map +1 -1
- package/dist/core/errors/index.d.ts +1 -1
- package/dist/core/errors/index.d.ts.map +1 -1
- package/dist/core/errors/index.js +1 -1
- package/dist/core/errors/index.js.map +1 -1
- package/dist/core/errors/namespace.d.ts +3 -1
- package/dist/core/errors/namespace.d.ts.map +1 -1
- package/dist/core/errors/namespace.js +3 -1
- package/dist/core/errors/namespace.js.map +1 -1
- package/dist/data/migrate/generate.d.ts.map +1 -1
- package/dist/data/migrate/generate.js +2 -1
- package/dist/data/migrate/generate.js.map +1 -1
- package/dist/data/migrate/internal-definitions.d.ts.map +1 -1
- package/dist/data/migrate/internal-definitions.js +9 -0
- package/dist/data/migrate/internal-definitions.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/email/audit.d.ts +3 -0
- package/dist/infra/email/audit.d.ts.map +1 -0
- package/dist/infra/email/audit.js +25 -0
- package/dist/infra/email/audit.js.map +1 -0
- package/dist/infra/email/index.d.ts +5 -0
- package/dist/infra/email/index.d.ts.map +1 -0
- package/dist/infra/email/index.js +5 -0
- package/dist/infra/email/index.js.map +1 -0
- package/dist/infra/email/mail.d.ts +69 -0
- package/dist/infra/email/mail.d.ts.map +1 -0
- package/dist/infra/email/mail.js +144 -0
- package/dist/infra/email/mail.js.map +1 -0
- package/dist/infra/email/namespace.d.ts +15 -0
- package/dist/infra/email/namespace.d.ts.map +1 -0
- package/dist/infra/email/namespace.js +15 -0
- package/dist/infra/email/namespace.js.map +1 -0
- package/dist/infra/email/providers.d.ts +42 -0
- package/dist/infra/email/providers.d.ts.map +1 -0
- package/dist/infra/email/providers.js +71 -0
- package/dist/infra/email/providers.js.map +1 -0
- package/dist/infra/email/schema.d.ts +74 -0
- package/dist/infra/email/schema.d.ts.map +1 -0
- package/dist/infra/email/schema.js +39 -0
- package/dist/infra/email/schema.js.map +1 -0
- package/dist/infra/email/sigv4.d.ts +24 -0
- package/dist/infra/email/sigv4.d.ts.map +1 -0
- package/dist/infra/email/sigv4.js +55 -0
- package/dist/infra/email/sigv4.js.map +1 -0
- package/dist/infra/email/sns.d.ts +32 -0
- package/dist/infra/email/sns.d.ts.map +1 -0
- package/dist/infra/email/sns.js +173 -0
- package/dist/infra/email/sns.js.map +1 -0
- package/dist/request/http/server.d.ts.map +1 -1
- package/dist/request/http/server.js +3 -0
- package/dist/request/http/server.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.63.0
|
|
4
|
+
|
|
5
|
+
**Schema note: blueprint `email: {}` adds `pipework_email_messages` (`key` unique, `to_address`, `template`, `data`, `status`, `provider_message_id`, `error`, `trace_id`, `created_at`, `sent_at`, `sending_at`) and `pipework_email_suppressions` (`address` primary key, `reason`, `detail`, `created_at`) to generated migrations; not tenant-scoped. Run `pipework generate` and `pipework migrate` to add them.**
|
|
6
|
+
|
|
7
|
+
- **New: `mail.create` sends transactional email through your job queue.** `send(db, { to, template, data, key })` records the message and enqueues its job in the caller's transaction (a rollback sends nothing); the same `key` sends once. Templates stay in the app as functions returning `{ subject, text, html? }`. Spread `mail.jobs` into your worker surface; the send runs in the trace of the flow that called `send`.
|
|
8
|
+
- **New: providers.** `mail.ses({ region, credentials, configurationSet? })` calls the SES v2 API with SigV4 (no AWS SDK); `mail.devStandIn({ dir })` writes each message to a file and `mail.create` refuses it when the environment is production.
|
|
9
|
+
- **New: bounces and complaints suppress the address.** `mail.snsWebhook({ path, topicArns, region? })` receives SES events through SNS, verifies the SNS signature, certificate host and topic, confirms the subscription, and puts the recipient of a permanent bounce or a complaint on a do-not-send list the job checks. Each one is audited (`Email`: `bounce`, `complaint`) with the original send's trace id. EventBridge delivery is not supported.
|
|
10
|
+
|
|
11
|
+
## 0.62.0
|
|
12
|
+
|
|
13
|
+
**Schema note: blueprint `passwords: {}` adds `pipework_signin_attempts` to generated migrations (`key` text primary key, `failures`, `first_failure_at`, `last_failure_at`, `locked_until`; not tenant-scoped). Run `pipework generate` and `pipework migrate` to add it.**
|
|
14
|
+
|
|
15
|
+
- **New: `auth.passwords` hashes and verifies passwords.** scrypt with a PHC-string hash that records its parameters, constant-time verify, and `needsRehash` when the configured parameters were raised. No composition rules; passwords up to 4096 bytes (long passphrases, pasted passwords) work. `verifyNone` costs the same as a wrong password, so an unknown account is not distinguishable by timing.
|
|
16
|
+
- **New: `auth.signIn` slows, then refuses, repeated failed sign-ins.** `attempt({ account, client?, password, lookup })` runs in the current Flow but outside the request transaction, so a route can throw `UnauthorizedError` and the count stays. Attempts are counted per account and per client address in Postgres (shared across processes, surviving restarts) before the password is checked, so parallel guesses are locked out; past the threshold each failure locks for a doubling, capped time and attempts are refused with `retryAfterSeconds`. A success resets the account's count. Failures, refusals and successes are logged and audited (`SignIn`: `fail`, `locked`, `success`).
|
|
17
|
+
- **New: `errors.TooManyRequests` (`TooManyRequestsError`, 429).** The HTTP server sends `Retry-After` when an error carries `retryAfterSeconds`.
|
|
18
|
+
|
|
3
19
|
## 0.61.0
|
|
4
20
|
|
|
5
21
|
**Schema note: apps using `auth.createSessions` / `auth.createMultiOrg` can now get their tables from `pipework generate`. Existing tables from the hand-copied SQL keep working (the statements are `IF NOT EXISTS`); to type their ids uuid, run `ALTER TABLE pipework_refresh_tokens ALTER COLUMN tenant_id TYPE uuid USING tenant_id::uuid` and `ALTER TABLE pipework_user_orgs ALTER COLUMN org_id TYPE uuid USING org_id::uuid` once.**
|
package/REFERENCE.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
- [audit — Audit Logging](#audit-audit-logging)
|
|
28
28
|
- [cache — Caching](#cache-caching)
|
|
29
29
|
- [webhook — Inbound & Outbound Webhooks](#webhook-inbound-outbound-webhooks)
|
|
30
|
+
- [mail — Transactional Email](#mail-transactional-email)
|
|
30
31
|
- [accessLinks — Access Links](#accesslinks-access-links)
|
|
31
32
|
- [behavior — Resource Decorators](#behavior-resource-decorators)
|
|
32
33
|
- [fitting — Dependency Injection](#fitting-dependency-injection)
|
|
@@ -292,6 +293,24 @@ Creates multi-org session management — org selection, switching, membership CR
|
|
|
292
293
|
|
|
293
294
|
Opt in with blueprint `orgs: {}` (plus `sessions: {}`). The generated migration then includes `pipework_user_orgs` (`user_id` text, `org_id` uuid, `role`, `joined_at`, `added_by`, unique on `(user_id, org_id)`). It is deliberately not tenant-scoped: a user's orgs are read across orgs at sign-in. An `orgId` that is not a uuid throws a `ValidationError`. The statements are `IF NOT EXISTS`; if your existing table has `org_id text` holding uuids, run once: `ALTER TABLE pipework_user_orgs ALTER COLUMN org_id TYPE uuid USING org_id::uuid`. The definition is for the default table name only: with a custom `table`, the app owns that table's DDL.
|
|
294
295
|
|
|
296
|
+
### `auth.passwords`
|
|
297
|
+
|
|
298
|
+
Hashes and verifies passwords with scrypt; reports when a stored hash needs rehashing.
|
|
299
|
+
|
|
300
|
+
`auth.passwords({ scrypt?: { ln, r, p } })` returns `{ hash, verify, verifyNone }`. Defaults are `ln=15, r=8, p=3`. `hash(password)` returns a PHC string `$scrypt$ln=15,r=8,p=3$<salt>$<hash>` (16-byte salt, 32-byte key). Passwords are NFC-normalized; there are no composition rules, and up to 4096 bytes of UTF-8 are accepted (more throws a `ValidationError`), so long passphrases and pasted passwords work. `verify(stored, password)` compares in constant time and returns `{ ok, needsRehash }`; `needsRehash` is true when the password matched but the stored parameters differ from the configured ones. A malformed stored string returns `ok: false` and logs an error. `verifyNone(password)` spends the cost of a verify against a fixed dummy hash, so an unknown account takes as long as a wrong password.
|
|
301
|
+
|
|
302
|
+
### `auth.signIn`
|
|
303
|
+
|
|
304
|
+
Guarded password sign-in that slows, then refuses, repeated failures per account and per client.
|
|
305
|
+
|
|
306
|
+
Opt in with blueprint `passwords: {}`; the generated migration adds `pipework_signin_attempts` (`key` text primary key, `failures`, `first_failure_at`, `last_failure_at`, `locked_until`), `IF NOT EXISTS`, not tenant-scoped (a sign-in happens before any tenant is known). Counts live in Postgres, so they survive restarts and are shared across processes. `attempt` needs an active Flow and runs every statement on `pipe.system()`, outside the request transaction, so the count is kept even when the route answers a bad password by throwing `UnauthorizedError`.
|
|
307
|
+
|
|
308
|
+
`auth.signIn({ passwords, accountThreshold = 5, clientThreshold = 20, windowSeconds = 900, maxLockSeconds = 900 })` returns `{ attempt({ account, client?, password, lookup }) }`, where `lookup(db, account)` (given `pipe.system()`, a pre-tenant read) returns `{ userId, passwordHash }` or `null`. The result is `{ ok: true, userId, needsRehash }` or `{ ok: false, reason: 'invalid' | 'locked', retryAfterSeconds? }`. An unknown account runs a dummy verify and is reported as `'invalid'`, exactly like a wrong password. Keys are `account:<sha256 of the NFC-lowercased trimmed account>` and `client:<client>` (omitted when no client is given). At or above its threshold, each further failure locks the key for `2^(failures - threshold)` seconds, capped at `maxLockSeconds`; a failure more than `windowSeconds` after the first restarts the count. Each attempt is counted, atomically per key, before the password is checked, so a burst of parallel guesses is locked out rather than all verified. While any key is locked an attempt returns `reason: 'locked'` with `retryAfterSeconds` at once, without checking the password. A success deletes the account key and gives the client key its count back.
|
|
309
|
+
|
|
310
|
+
Tradeoff: locking is per account, so anyone who knows an account name can lock it out by failing on purpose. Each lock is capped (15 minutes by default), and the client key limits how fast one address can do it.
|
|
311
|
+
|
|
312
|
+
Every success, failure and refusal is logged, and audited (entity `SignIn`, verbs `success`, `fail`, `locked`; `entity_id` is the user id when known; `after` holds `accountKey`, `client`, `failures`, `retryAfterSeconds`, never the account string or password) when the Flow has an open trace step. For a route, turn a locked result into a 429 with `throw new errors.TooManyRequests(result.retryAfterSeconds)`.
|
|
313
|
+
|
|
295
314
|
### `auth.runChain`
|
|
296
315
|
|
|
297
316
|
Executes an auth chain (strategy pipeline) to resolve auth + tenant from a request.
|
|
@@ -586,6 +605,10 @@ Base error class with code, statusCode, and suggestion fields.
|
|
|
586
605
|
|
|
587
606
|
409 — resource state conflict (duplicate, version mismatch).
|
|
588
607
|
|
|
608
|
+
### `errors.TooManyRequests`
|
|
609
|
+
|
|
610
|
+
429 — too many attempts; carries `retryAfterSeconds`, sent as a `Retry-After` header.
|
|
611
|
+
|
|
589
612
|
## lifecycle — Startup & Shutdown
|
|
590
613
|
|
|
591
614
|
Application lifecycle namespace — startup orchestration with hooks and timeouts.
|
|
@@ -1097,6 +1120,36 @@ Creates an outbound webhook system — endpoint registration, payload signing, d
|
|
|
1097
1120
|
|
|
1098
1121
|
Signs a payload with HMAC for outbound delivery — returns body, signature, timestamp, messageId.
|
|
1099
1122
|
|
|
1123
|
+
## mail — Transactional Email
|
|
1124
|
+
|
|
1125
|
+
Mail namespace — transactional email through a job queue, SES v2 or a dev stand-in, bounce and complaint suppression.
|
|
1126
|
+
|
|
1127
|
+
Opt in with blueprint `email: {}`; the generated migration adds `pipework_email_messages` (`key` unique, `to_address`, `template`, `data`, `status` `queued | sending | sent | suppressed | failed`, `provider_message_id`, `error`, `trace_id`, `created_at`, `sent_at`, `sending_at`) and `pipework_email_suppressions` (`address` primary key, lowercased; `reason` `bounce | complaint`; `detail`; `created_at`), both `IF NOT EXISTS` and not tenant-scoped.
|
|
1128
|
+
|
|
1129
|
+
### `mail.create`
|
|
1130
|
+
|
|
1131
|
+
Creates the transactional email interface: `send(db, { to, template, data, key })` in the caller's transaction, and the `jobs` that deliver.
|
|
1132
|
+
|
|
1133
|
+
`mail.create({ provider, templates, from, queue, configurationSet?, environment?, sendingStaleSeconds? })` returns `{ send, jobs }`. `templates` is `{ [name]: (data) => { subject, text, html? } }`; `send` checks the template name and the data type. `send(db, ...)` inserts the message row (`ON CONFLICT (key) DO NOTHING`) and enqueues a `pipework.email.send` job on `queue` in the same `db`, so the caller's rollback drops both; it returns `{ messageId, queued }`, with `queued: false` and the earlier id when the key was used. Spread `mail.jobs` into the worker surface's `jobs` (on the same queue). The job skips a message already `sent`, `suppressed` or `failed`, marks an address on the suppression list `suppressed` without calling the provider, and otherwise renders, sends and marks `sent`. A provider error returns the row to `queued` with `error` set and rethrows, so the queue retries per its `maxAttempts`; if it gives up, the row keeps the error. A template that is missing or throws marks the row `failed` without retrying. The job claims the message with a conditional update (`queued`, or `sending` for longer than `sendingStaleSeconds`, default 600), so a reaper re-run of a job still in flight does not call the provider; a run that cannot claim returns and logs at info. Residual window: a crash mid-send leaves the message `sending` for the stale window, after which a retry may send it once more; likewise if the provider accepted the message and the process died before the row was marked `sent`. SES v2 has no idempotency token. The job runs in the trace of the flow that called `send` (`continueFlow`); the delivery is the step `pipework/email:deliver`, with `Email:sent`, `Email:suppressed` or `Email:failed` audit records under it. Message bodies and template data are never logged. `create` throws when `environment` is `production` and the provider is the stand-in; the job checks the instance's environment too.
|
|
1134
|
+
|
|
1135
|
+
### `mail.ses`
|
|
1136
|
+
|
|
1137
|
+
Amazon SES v2 provider, SigV4-signed without the AWS SDK.
|
|
1138
|
+
|
|
1139
|
+
`mail.ses({ region, credentials: async () => ({ accessKeyId, secretAccessKey, sessionToken? }), configurationSet?, endpoint? })` posts JSON to `https://email.<region>.amazonaws.com/v2/email/outbound-emails` (`endpoint` replaces the base URL, for tests). Credentials come from the app, never from the environment, and are asked for on every send. A non-2xx answer throws with the status and AWS's message.
|
|
1140
|
+
|
|
1141
|
+
### `mail.devStandIn`
|
|
1142
|
+
|
|
1143
|
+
Development provider that writes each message to a file instead of sending; refused in production.
|
|
1144
|
+
|
|
1145
|
+
`mail.devStandIn({ dir })` writes `<dir>/<messageId>.json` (`from`, `to`, `subject`, `text`, `html`) and logs one line with the address and file path.
|
|
1146
|
+
|
|
1147
|
+
### `mail.snsWebhook`
|
|
1148
|
+
|
|
1149
|
+
Inbound handler for SES bounce and complaint events delivered through SNS; suppresses the address.
|
|
1150
|
+
|
|
1151
|
+
`mail.snsWebhook({ path, topicArns, region?, fetchCert?, confirm? })` returns a public POST handler for an SNS HTTP/S subscription to the SES event topic. `topicArns` is required: after the signature verifies, a message whose `TopicArn` is not listed is refused with 401, before a subscription is confirmed or an event processed (SNS signs every topic, so anyone could otherwise subscribe the endpoint to their own topic and forge bounces). Every message's signature (SignatureVersion 1 or 2) is verified against the certificate at `SigningCertURL`, which must be `https://sns.<region>.amazonaws.com/...pem` (any AWS region when `region` is omitted); otherwise the answer is 401. A `SubscriptionConfirmation` is confirmed by fetching its `SubscribeURL` (same host rule). A `Notification` with a permanent bounce or a complaint puts each recipient on `pipework_email_suppressions`; a transient or undetermined bounce is logged only. Each is the step `pipework/email:notification` with an `Email:bounce` or `Email:complaint` audit record (`entity_id` the message id when found by `provider_message_id`; `after` has `address`, `bounceType`, `suppressed`, `messageTraceId`, the trace of the original send). A suppression logs a warning `email suppressed`; the alert condition (#458) is not built yet. SES events delivered through EventBridge are not supported. `fetchCert` and `confirm` replace the default HTTP fetches (for tests).
|
|
1152
|
+
|
|
1100
1153
|
## accessLinks — Access Links
|
|
1101
1154
|
|
|
1102
1155
|
Short, revocable bearer links to a tenant's resource, for people with no account ("a public link that never expires", "a link for John that works once"). A tenant creates a link; anyone holding the code can resolve it on a public route with no tenant and no auth in the Flow; a use counts only when the app confirms it completed.
|
package/dist/REFERENCE.md
CHANGED
|
@@ -277,6 +277,14 @@ Creates a Sessions manager with JWT access/refresh tokens, rotation, and reuse d
|
|
|
277
277
|
|
|
278
278
|
Creates multi-org session management — org selection, switching, membership CRUD.
|
|
279
279
|
|
|
280
|
+
### `auth.passwords`
|
|
281
|
+
|
|
282
|
+
Hashes and verifies passwords with scrypt; reports when a stored hash needs rehashing.
|
|
283
|
+
|
|
284
|
+
### `auth.signIn`
|
|
285
|
+
|
|
286
|
+
Guarded password sign-in that slows, then refuses, repeated failures per account and per client.
|
|
287
|
+
|
|
280
288
|
### `auth.runChain`
|
|
281
289
|
|
|
282
290
|
Executes an auth chain (strategy pipeline) to resolve auth + tenant from a request.
|
|
@@ -377,6 +385,54 @@ interface Sessions
|
|
|
377
385
|
interface CookieConfig
|
|
378
386
|
```
|
|
379
387
|
|
|
388
|
+
**`ScryptParams`**
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
interface ScryptParams
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**`PasswordsConfig`**
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
interface PasswordsConfig
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**`VerifyResult`**
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
interface VerifyResult
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**`Passwords`**
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
interface Passwords
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**`SignInConfig`**
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
interface SignInConfig
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**`SignInAttempt`**
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
interface SignInAttempt
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**`SignInResult`**
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
type SignInResult =
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**`SignIn`**
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
interface SignIn
|
|
434
|
+
```
|
|
435
|
+
|
|
380
436
|
**`MultiOrgConfig`** — Configuration for multi-org sessions — base sessions, signing keys, org-select token TTL.
|
|
381
437
|
|
|
382
438
|
```typescript
|
|
@@ -572,6 +628,10 @@ Base error class with code, statusCode, and suggestion fields.
|
|
|
572
628
|
|
|
573
629
|
409 — resource state conflict (duplicate, version mismatch).
|
|
574
630
|
|
|
631
|
+
### `errors.TooManyRequests`
|
|
632
|
+
|
|
633
|
+
429 — too many attempts; carries `retryAfterSeconds`, sent as a `Retry-After` header.
|
|
634
|
+
|
|
575
635
|
## lifecycle — Startup & Shutdown
|
|
576
636
|
|
|
577
637
|
Application lifecycle namespace — startup orchestration with hooks and timeouts.
|
|
@@ -703,7 +763,7 @@ Field builders for domain definitions — uuid(), text(), integer(), etc.
|
|
|
703
763
|
|
|
704
764
|
### `pipe.define`
|
|
705
765
|
|
|
706
|
-
Registers a domain definition with table, validators, and factory projections.
|
|
766
|
+
Registers a domain definition with table, validators, and factory projections. The `auditEntity` option names the `@touches` entity writes to this table are audited under (see `__flowStep`).
|
|
707
767
|
|
|
708
768
|
### `pipe.definitions`
|
|
709
769
|
|
|
@@ -1037,7 +1097,11 @@ Creates an Audit instance that writes audit records to a Postgres table.
|
|
|
1037
1097
|
|
|
1038
1098
|
### `audit.query`
|
|
1039
1099
|
|
|
1040
|
-
Queries the framework-managed `audit_record` table. Tenant-scoped via the active Flow.
|
|
1100
|
+
Queries the framework-managed `audit_record` table. Tenant-scoped via the active Flow. `{ entity, entityId }` returns one row's history from an index; build a composite key's `entityId` with `audit.entityId`.
|
|
1101
|
+
|
|
1102
|
+
### `audit.entityId`
|
|
1103
|
+
|
|
1104
|
+
The `entityId` filter text for a written table's row: the primary key's value as text, or for a composite key a compact JSON array of the key values as strings in key order (`["a","7"]`). Pass the table and a row (or just its key fields). Dates become ISO strings.
|
|
1041
1105
|
|
|
1042
1106
|
### `audit.chainHead`
|
|
1043
1107
|
|
|
@@ -1209,7 +1273,7 @@ interface ErrorResponse
|
|
|
1209
1273
|
interface PipeworkServer
|
|
1210
1274
|
```
|
|
1211
1275
|
|
|
1212
|
-
**`HttpRequest`** — Fastify request with pipework extensions — req.id (UUID), typed headers.
|
|
1276
|
+
**`HttpRequest`** — Fastify request with pipework extensions — req.id (UUID), typed headers, client address (`ip`, `ips`).
|
|
1213
1277
|
|
|
1214
1278
|
```typescript
|
|
1215
1279
|
interface HttpRequest
|
|
@@ -7,4 +7,8 @@ export type { CookieConfig, ResolvedCookieConfig } from './cookie.js';
|
|
|
7
7
|
export { resolveCookieConfig, buildSetCookieHeader, buildClearCookieHeader, parseCookieHeader } from './cookie.js';
|
|
8
8
|
export { createMultiOrgSessions } from './multi-org.js';
|
|
9
9
|
export type { MultiOrgConfig, MultiOrgSessions, ResolveLoginResult, AuthenticatedResult, OrgSelectResult, OrgMembership, OrgInfo, OrgMembershipDetail } from './multi-org.js';
|
|
10
|
+
export { createPasswords } from './passwords.js';
|
|
11
|
+
export type { Passwords, PasswordsConfig, ScryptParams, VerifyResult } from './passwords.js';
|
|
12
|
+
export { createSignIn } from './sign-in.js';
|
|
13
|
+
export type { SignIn, SignInConfig, SignInAttempt, SignInResult } from './sign-in.js';
|
|
10
14
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAChG,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjI,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACrE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,aAAa,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAChG,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjI,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACrE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,eAAe,EAAE,aAAa,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAC7K,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC5F,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/auth/auth/index.js
CHANGED
|
@@ -2,4 +2,6 @@ export { runAuthChain } from './chain.js';
|
|
|
2
2
|
export { createSessions } from './sessions.js';
|
|
3
3
|
export { resolveCookieConfig, buildSetCookieHeader, buildClearCookieHeader, parseCookieHeader } from './cookie.js';
|
|
4
4
|
export { createMultiOrgSessions } from './multi-org.js';
|
|
5
|
+
export { createPasswords } from './passwords.js';
|
|
6
|
+
export { createSignIn } from './sign-in.js';
|
|
5
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/auth/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG9C,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/auth/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG9C,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAClH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSessions } from './sessions.js';
|
|
2
2
|
import { createMultiOrgSessions } from './multi-org.js';
|
|
3
|
+
import { createPasswords } from './passwords.js';
|
|
4
|
+
import { createSignIn } from './sign-in.js';
|
|
3
5
|
import { runAuthChain } from './chain.js';
|
|
4
6
|
import { resolveCookieConfig, parseCookieHeader, buildSetCookieHeader, buildClearCookieHeader } from './cookie.js';
|
|
5
7
|
/** Authentication namespace — session management, multi-org support, auth chain resolution. */
|
|
@@ -8,6 +10,10 @@ export declare const auth: {
|
|
|
8
10
|
createSessions: typeof createSessions;
|
|
9
11
|
/** Creates multi-org session management — org selection, switching, membership CRUD. */
|
|
10
12
|
createMultiOrg: typeof createMultiOrgSessions;
|
|
13
|
+
/** Hashes and verifies passwords with scrypt; reports when a stored hash needs rehashing. */
|
|
14
|
+
passwords: typeof createPasswords;
|
|
15
|
+
/** Guarded password sign-in that slows, then refuses, repeated failures per account and per client. */
|
|
16
|
+
signIn: typeof createSignIn;
|
|
11
17
|
/** Executes an auth chain (strategy pipeline) to resolve auth + tenant from a request. */
|
|
12
18
|
runChain: typeof runAuthChain;
|
|
13
19
|
/** Resolves CookieConfig with defaults for secure cookie-based token delivery. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"namespace.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/namespace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAElH,+FAA+F;AAC/F,eAAO,MAAM,IAAI;IACf,gGAAgG;;IAEhG,wFAAwF;;IAExF,0FAA0F;;IAE1F,kFAAkF;;IAElF,0DAA0D;;IAE1D,4EAA4E;;IAE5E,iEAAiE;;CAElE,CAAA"}
|
|
1
|
+
{"version":3,"file":"namespace.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/namespace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAElH,+FAA+F;AAC/F,eAAO,MAAM,IAAI;IACf,gGAAgG;;IAEhG,wFAAwF;;IAExF,6FAA6F;;IAE7F,uGAAuG;;IAEvG,0FAA0F;;IAE1F,kFAAkF;;IAElF,0DAA0D;;IAE1D,4EAA4E;;IAE5E,iEAAiE;;CAElE,CAAA"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSessions } from './sessions.js';
|
|
2
2
|
import { createMultiOrgSessions } from './multi-org.js';
|
|
3
|
+
import { createPasswords } from './passwords.js';
|
|
4
|
+
import { createSignIn } from './sign-in.js';
|
|
3
5
|
import { runAuthChain } from './chain.js';
|
|
4
6
|
import { resolveCookieConfig, parseCookieHeader, buildSetCookieHeader, buildClearCookieHeader } from './cookie.js';
|
|
5
7
|
/** Authentication namespace — session management, multi-org support, auth chain resolution. */
|
|
@@ -8,6 +10,10 @@ export const auth = {
|
|
|
8
10
|
createSessions,
|
|
9
11
|
/** Creates multi-org session management — org selection, switching, membership CRUD. */
|
|
10
12
|
createMultiOrg: createMultiOrgSessions,
|
|
13
|
+
/** Hashes and verifies passwords with scrypt; reports when a stored hash needs rehashing. */
|
|
14
|
+
passwords: createPasswords,
|
|
15
|
+
/** Guarded password sign-in that slows, then refuses, repeated failures per account and per client. */
|
|
16
|
+
signIn: createSignIn,
|
|
11
17
|
/** Executes an auth chain (strategy pipeline) to resolve auth + tenant from a request. */
|
|
12
18
|
runChain: runAuthChain,
|
|
13
19
|
/** Resolves CookieConfig with defaults for secure cookie-based token delivery. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"namespace.js","sourceRoot":"","sources":["../../../src/auth/auth/namespace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAElH,+FAA+F;AAC/F,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,gGAAgG;IAChG,cAAc;IACd,wFAAwF;IACxF,cAAc,EAAE,sBAAsB;IACtC,0FAA0F;IAC1F,QAAQ,EAAE,YAAY;IACtB,kFAAkF;IAClF,aAAa,EAAE,mBAAmB;IAClC,0DAA0D;IAC1D,WAAW,EAAE,iBAAiB;IAC9B,4EAA4E;IAC5E,cAAc,EAAE,oBAAoB;IACpC,iEAAiE;IACjE,gBAAgB,EAAE,sBAAsB;CACzC,CAAA"}
|
|
1
|
+
{"version":3,"file":"namespace.js","sourceRoot":"","sources":["../../../src/auth/auth/namespace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAElH,+FAA+F;AAC/F,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,gGAAgG;IAChG,cAAc;IACd,wFAAwF;IACxF,cAAc,EAAE,sBAAsB;IACtC,6FAA6F;IAC7F,SAAS,EAAE,eAAe;IAC1B,uGAAuG;IACvG,MAAM,EAAE,YAAY;IACpB,0FAA0F;IAC1F,QAAQ,EAAE,YAAY;IACtB,kFAAkF;IAClF,aAAa,EAAE,mBAAmB;IAClC,0DAA0D;IAC1D,WAAW,EAAE,iBAAiB;IAC9B,4EAA4E;IAC5E,cAAc,EAAE,oBAAoB;IACpC,iEAAiE;IACjE,gBAAgB,EAAE,sBAAsB;CACzC,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ScryptParams {
|
|
2
|
+
/** log2 of the CPU/memory cost N. Default 15. */
|
|
3
|
+
readonly ln: number;
|
|
4
|
+
/** Block size. Default 8. */
|
|
5
|
+
readonly r: number;
|
|
6
|
+
/** Parallelism. Default 3. */
|
|
7
|
+
readonly p: number;
|
|
8
|
+
}
|
|
9
|
+
export interface PasswordsConfig {
|
|
10
|
+
readonly scrypt?: Partial<ScryptParams>;
|
|
11
|
+
}
|
|
12
|
+
export interface VerifyResult {
|
|
13
|
+
readonly ok: boolean;
|
|
14
|
+
/** True when the password matched but the stored hash used other parameters than the configured ones. */
|
|
15
|
+
readonly needsRehash: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface Passwords {
|
|
18
|
+
/** Returns a PHC string `$scrypt$ln=15,r=8,p=3$<salt>$<hash>` for the password. */
|
|
19
|
+
hash(password: string): Promise<string>;
|
|
20
|
+
/** Checks a password against a stored hash in constant time. A malformed stored hash is `ok: false`. */
|
|
21
|
+
verify(stored: string, password: string): Promise<VerifyResult>;
|
|
22
|
+
/** Spends the cost of a verify against a fixed dummy hash, for an account that does not exist. */
|
|
23
|
+
verifyNone(password: string): Promise<{
|
|
24
|
+
ok: false;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates the password hasher. Passwords are NFC-normalized and hashed with
|
|
29
|
+
* Node's scrypt (no dependency); there are no composition rules, and anything up
|
|
30
|
+
* to 4096 bytes of UTF-8 is accepted, so long passphrases and pasted passwords
|
|
31
|
+
* work. The hash records its own parameters, so raising `scrypt` later leaves old
|
|
32
|
+
* hashes verifiable and `verify` reports `needsRehash` for them.
|
|
33
|
+
*
|
|
34
|
+
* `verify` never throws on a bad stored string (it returns `ok: false` and logs),
|
|
35
|
+
* and `verifyNone` costs the same as a wrong password, so an unknown account is
|
|
36
|
+
* not distinguishable by timing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createPasswords(config?: PasswordsConfig): Passwords;
|
|
39
|
+
//# sourceMappingURL=passwords.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwords.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/passwords.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,6BAA6B;IAC7B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,8BAA8B;IAC9B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,yGAAyG;IACzG,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,SAAS;IACxB,mFAAmF;IACnF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IACvC,wGAAwG;IACxG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC/D,kGAAkG;IAClG,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,KAAK,CAAA;KAAE,CAAC,CAAA;CACrD;AAgCD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,GAAE,eAAoB,GAAG,SAAS,CAoCvE"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { ValidationError } from '../../core/errors/index.js';
|
|
3
|
+
import { getBaseLogger } from '../../core/logging/index.js';
|
|
4
|
+
const DEFAULTS = { ln: 15, r: 8, p: 3 };
|
|
5
|
+
const KEY_LENGTH = 32;
|
|
6
|
+
const SALT_LENGTH = 16;
|
|
7
|
+
const MAX_PASSWORD_BYTES = 4096;
|
|
8
|
+
const STORED_RE = /^\$scrypt\$ln=(\d+),r=(\d+),p=(\d+)\$([A-Za-z0-9+/]+)\$([A-Za-z0-9+/]+)$/;
|
|
9
|
+
function derive(password, salt, params, length) {
|
|
10
|
+
const N = 2 ** params.ln;
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
scrypt(password, salt, length, { N, r: params.r, p: params.p, maxmem: 256 * N * params.r + (1 << 20) }, (err, key) => {
|
|
13
|
+
if (err !== null)
|
|
14
|
+
reject(err);
|
|
15
|
+
else
|
|
16
|
+
resolve(key);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function normalize(password) {
|
|
21
|
+
const normalized = password.normalize('NFC');
|
|
22
|
+
if (Buffer.byteLength(normalized, 'utf8') > MAX_PASSWORD_BYTES) {
|
|
23
|
+
throw new ValidationError({ password: [`must be at most ${MAX_PASSWORD_BYTES} bytes`] }, 'Password is too long.');
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
function validParams(p) {
|
|
28
|
+
return Number.isInteger(p.ln) && p.ln >= 1 && p.ln <= 22
|
|
29
|
+
&& Number.isInteger(p.r) && p.r >= 1 && p.r <= 32
|
|
30
|
+
&& Number.isInteger(p.p) && p.p >= 1 && p.p <= 16;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Creates the password hasher. Passwords are NFC-normalized and hashed with
|
|
34
|
+
* Node's scrypt (no dependency); there are no composition rules, and anything up
|
|
35
|
+
* to 4096 bytes of UTF-8 is accepted, so long passphrases and pasted passwords
|
|
36
|
+
* work. The hash records its own parameters, so raising `scrypt` later leaves old
|
|
37
|
+
* hashes verifiable and `verify` reports `needsRehash` for them.
|
|
38
|
+
*
|
|
39
|
+
* `verify` never throws on a bad stored string (it returns `ok: false` and logs),
|
|
40
|
+
* and `verifyNone` costs the same as a wrong password, so an unknown account is
|
|
41
|
+
* not distinguishable by timing.
|
|
42
|
+
*/
|
|
43
|
+
export function createPasswords(config = {}) {
|
|
44
|
+
const params = { ...DEFAULTS, ...config.scrypt };
|
|
45
|
+
if (!validParams(params)) {
|
|
46
|
+
throw new ValidationError('auth.passwords scrypt params must satisfy 1<=ln<=22, 1<=r<=32, 1<=p<=16.');
|
|
47
|
+
}
|
|
48
|
+
const encode = (b) => b.toString('base64').replace(/=+$/, '');
|
|
49
|
+
let dummy;
|
|
50
|
+
async function hash(password) {
|
|
51
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
52
|
+
const key = await derive(normalize(password), salt, params, KEY_LENGTH);
|
|
53
|
+
return `$scrypt$ln=${params.ln},r=${params.r},p=${params.p}$${encode(salt)}$${encode(key)}`;
|
|
54
|
+
}
|
|
55
|
+
async function verify(stored, password) {
|
|
56
|
+
const m = STORED_RE.exec(stored);
|
|
57
|
+
const storedParams = m === null ? null : { ln: Number(m[1]), r: Number(m[2]), p: Number(m[3]) };
|
|
58
|
+
if (m === null || storedParams === null || !validParams(storedParams)) {
|
|
59
|
+
getBaseLogger().error({ event: 'password hash malformed' }, 'stored password hash is malformed');
|
|
60
|
+
return { ok: false, needsRehash: false };
|
|
61
|
+
}
|
|
62
|
+
const salt = Buffer.from(m[4], 'base64');
|
|
63
|
+
const expected = Buffer.from(m[5], 'base64');
|
|
64
|
+
const key = await derive(normalize(password), salt, storedParams, expected.length);
|
|
65
|
+
const ok = key.length === expected.length && timingSafeEqual(key, expected);
|
|
66
|
+
const needsRehash = ok && (storedParams.ln !== params.ln || storedParams.r !== params.r || storedParams.p !== params.p);
|
|
67
|
+
return { ok, needsRehash };
|
|
68
|
+
}
|
|
69
|
+
async function verifyNone(password) {
|
|
70
|
+
dummy ??= hash(randomBytes(16).toString('hex'));
|
|
71
|
+
await verify(await dummy, password);
|
|
72
|
+
return { ok: false };
|
|
73
|
+
}
|
|
74
|
+
return { hash, verify, verifyNone };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=passwords.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passwords.js","sourceRoot":"","sources":["../../../src/auth/auth/passwords.ts"],"names":[],"mappings":"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"}
|
|
@@ -74,4 +74,31 @@ export declare const userOrgs: import("../../data/domain/types.js").DefinedTable
|
|
|
74
74
|
nullable: true;
|
|
75
75
|
}>;
|
|
76
76
|
}, "pipework_user_orgs">;
|
|
77
|
+
export declare const signinAttempts: import("../../data/domain/types.js").DefinedTable<{
|
|
78
|
+
key: import("../../data/domain/types.js").FieldDescriptor<string, {
|
|
79
|
+
primaryKey: true;
|
|
80
|
+
}> & import("../../data/domain/field.js").FieldModifiers<string, {
|
|
81
|
+
primaryKey: true;
|
|
82
|
+
}>;
|
|
83
|
+
failures: import("../../data/domain/types.js").FieldDescriptor<number, {
|
|
84
|
+
hasDefault: true;
|
|
85
|
+
}> & import("../../data/domain/field.js").FieldModifiers<number, {
|
|
86
|
+
hasDefault: true;
|
|
87
|
+
}>;
|
|
88
|
+
firstFailureAt: import("../../data/domain/types.js").FieldDescriptor<Date | null, {
|
|
89
|
+
nullable: true;
|
|
90
|
+
}> & import("../../data/domain/field.js").FieldModifiers<Date | null, {
|
|
91
|
+
nullable: true;
|
|
92
|
+
}>;
|
|
93
|
+
lastFailureAt: import("../../data/domain/types.js").FieldDescriptor<Date | null, {
|
|
94
|
+
nullable: true;
|
|
95
|
+
}> & import("../../data/domain/field.js").FieldModifiers<Date | null, {
|
|
96
|
+
nullable: true;
|
|
97
|
+
}>;
|
|
98
|
+
lockedUntil: import("../../data/domain/types.js").FieldDescriptor<Date | null, {
|
|
99
|
+
nullable: true;
|
|
100
|
+
}> & import("../../data/domain/field.js").FieldModifiers<Date | null, {
|
|
101
|
+
nullable: true;
|
|
102
|
+
}>;
|
|
103
|
+
}, "pipework_signin_attempts">;
|
|
77
104
|
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/schema.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAE/C,CAAA;AAEF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAexB,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;4BAIvB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAanB,CAAA"}
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/schema.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAE/C,CAAA;AAEF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAexB,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;4BAIvB,CAAA;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAanB,CAAA;AAIF,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;8BAMzB,CAAA"}
|
package/dist/auth/auth/schema.js
CHANGED
|
@@ -17,7 +17,7 @@ import { field } from '../../data/domain/field.js';
|
|
|
17
17
|
import { sql } from '../../data/db/sql.js';
|
|
18
18
|
import { index, unique } from '../../data/db/idx/index.js';
|
|
19
19
|
export const AUTH_TABLE_NAMES = new Set([
|
|
20
|
-
'pipework_refresh_tokens', 'pipework_auth_versions', 'pipework_user_orgs',
|
|
20
|
+
'pipework_refresh_tokens', 'pipework_auth_versions', 'pipework_user_orgs', 'pipework_signin_attempts',
|
|
21
21
|
]);
|
|
22
22
|
export const refreshTokens = define('pipework_refresh_tokens', {
|
|
23
23
|
id: field.uuid().defaultRandom().primaryKey(),
|
|
@@ -54,4 +54,13 @@ export const userOrgs = define('pipework_user_orgs', {
|
|
|
54
54
|
byOrg: index('idx_pipework_user_orgs_org').on(t['orgId']),
|
|
55
55
|
}),
|
|
56
56
|
});
|
|
57
|
+
// Behind `auth.signIn`. Not tenant-scoped: a sign-in happens before any tenant
|
|
58
|
+
// is known. `key` is 'account:<sha256 of the account>' or 'client:<address>'.
|
|
59
|
+
export const signinAttempts = define('pipework_signin_attempts', {
|
|
60
|
+
key: field.text().primaryKey(),
|
|
61
|
+
failures: field.integer().default(0),
|
|
62
|
+
firstFailureAt: field.timestamp().nullable(),
|
|
63
|
+
lastFailureAt: field.timestamp().nullable(),
|
|
64
|
+
lockedUntil: field.timestamp().nullable(),
|
|
65
|
+
});
|
|
57
66
|
//# sourceMappingURL=schema.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/auth/auth/schema.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,yEAAyE;AACzE,6EAA6E;AAC7E,4EAA4E;AAC5E,mDAAmD;AACnD,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,6EAA6E;AAC7E,gFAAgF;AAChF,qDAAqD;AACrD,EAAE;AACF,8EAA8E;AAC9E,6BAA6B;AAE7B,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAA;AAClD,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AAE1D,MAAM,CAAC,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC;IAC3D,yBAAyB,EAAE,wBAAwB,EAAE,oBAAoB;
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/auth/auth/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DB } from '../../data/db/index.js';
|
|
2
|
+
import type { Passwords } from './passwords.js';
|
|
3
|
+
export interface SignInConfig {
|
|
4
|
+
readonly passwords: Passwords;
|
|
5
|
+
/** Failures per account within the window before it is locked. Default 5. */
|
|
6
|
+
readonly accountThreshold?: number;
|
|
7
|
+
/** Failures per client address within the window before it is locked. Default 20. */
|
|
8
|
+
readonly clientThreshold?: number;
|
|
9
|
+
/** Window since the first failure after which the count restarts. Default 900 seconds. */
|
|
10
|
+
readonly windowSeconds?: number;
|
|
11
|
+
/** Longest single lock. Default 900 seconds. */
|
|
12
|
+
readonly maxLockSeconds?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SignInAttempt {
|
|
15
|
+
readonly account: string;
|
|
16
|
+
/** The client address; without it only the account is guarded. */
|
|
17
|
+
readonly client?: string;
|
|
18
|
+
readonly password: string;
|
|
19
|
+
/** Finds the account's user id and stored password hash, or null if there is no such account. */
|
|
20
|
+
readonly lookup: (db: DB, account: string) => Promise<{
|
|
21
|
+
userId: string;
|
|
22
|
+
passwordHash: string;
|
|
23
|
+
} | null>;
|
|
24
|
+
}
|
|
25
|
+
export type SignInResult = {
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
readonly userId: string;
|
|
28
|
+
readonly needsRehash: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
readonly ok: false;
|
|
31
|
+
readonly reason: 'invalid' | 'locked';
|
|
32
|
+
readonly retryAfterSeconds?: number;
|
|
33
|
+
};
|
|
34
|
+
export interface SignIn {
|
|
35
|
+
attempt(input: SignInAttempt): Promise<SignInResult>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a guarded password sign-in. `attempt` needs an active Flow (a request,
|
|
39
|
+
* job or test context) and runs every statement, `lookup` included, on
|
|
40
|
+
* `pipe.system()`: outside the request transaction, committed on its own, so a
|
|
41
|
+
* route that answers a bad password by throwing `UnauthorizedError` (rolling the
|
|
42
|
+
* request back) still keeps the count. Failed attempts are counted per account and
|
|
43
|
+
* per client address in `pipework_signin_attempts` (blueprint `passwords: {}`), so
|
|
44
|
+
* counts survive restarts and are shared across processes. At the threshold each
|
|
45
|
+
* further failure locks the key for 2^(failures - threshold) seconds, capped at
|
|
46
|
+
* `maxLockSeconds`. Every attempt is counted, atomically per key, before the
|
|
47
|
+
* password is checked, so a burst of parallel guesses is locked out instead of
|
|
48
|
+
* all being verified. While any key is locked an attempt is refused with
|
|
49
|
+
* `reason: 'locked'` and `retryAfterSeconds`, without checking the password (a
|
|
50
|
+
* locked key is not counted further). A success deletes the account's key and
|
|
51
|
+
* gives the client key its count back. An unknown account runs a dummy verify
|
|
52
|
+
* and counts as a failure, reported as `'invalid'` like a wrong password.
|
|
53
|
+
*
|
|
54
|
+
* Tradeoff: locking is per account, so anyone who knows an account name can lock
|
|
55
|
+
* it out by failing on purpose. Each lock is capped (default 15 minutes) and the
|
|
56
|
+
* client key bounds how fast one address can do it.
|
|
57
|
+
*
|
|
58
|
+
* Every success, failure and refusal is logged, and audited (entity `SignIn`,
|
|
59
|
+
* verbs `success`, `fail`, `locked`; never the account string or password) when
|
|
60
|
+
* the Flow has an open trace step. Route a locked result to a 429 with
|
|
61
|
+
* `throw new TooManyRequestsError(result.retryAfterSeconds)`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createSignIn(config: SignInConfig): SignIn;
|
|
64
|
+
//# sourceMappingURL=sign-in.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign-in.d.ts","sourceRoot":"","sources":["../../../src/auth/auth/sign-in.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAA;AAIhD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAClC,qFAAqF;IACrF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,0FAA0F;IAC1F,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;CACvG;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;CAAE,GAC7E;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtG,MAAM,WAAW,MAAM;IACrB,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CACrD;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA0GzD"}
|