@shipfox/api-auth 11.0.0 → 12.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +26 -0
- package/README.md +19 -19
- package/dist/core/job-lease-token.d.ts +1 -1
- package/dist/core/job-lease-token.d.ts.map +1 -1
- package/dist/core/job-lease-token.js +1 -1
- package/dist/core/job-lease-token.js.map +1 -1
- package/dist/core/runner-session-token.d.ts +1 -1
- package/dist/core/runner-session-token.d.ts.map +1 -1
- package/dist/core/runner-session-token.js +1 -1
- package/dist/core/runner-session-token.js.map +1 -1
- package/dist/db/rate-limits.d.ts +6 -15
- package/dist/db/rate-limits.d.ts.map +1 -1
- package/dist/db/rate-limits.js +17 -18
- package/dist/db/rate-limits.js.map +1 -1
- package/dist/db/refresh-tokens.d.ts +3 -3
- package/dist/db/refresh-tokens.js +3 -3
- package/dist/db/refresh-tokens.js.map +1 -1
- package/dist/presentation/auth/lease-token-auth.d.ts +1 -1
- package/dist/presentation/auth/lease-token-auth.js +1 -1
- package/dist/presentation/auth/lease-token-auth.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +32 -53
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/core/job-lease-token.ts +1 -1
- package/src/core/runner-session-token.ts +1 -1
- package/src/db/rate-limits.ts +28 -42
- package/src/db/refresh-tokens.ts +3 -3
- package/src/presentation/auth/lease-token-auth.ts +1 -1
- package/src/presentation/routes/rate-limit.ts +31 -64
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled: 67 files with swc (
|
|
2
|
+
Successfully compiled: 67 files with swc (326.91ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @shipfox/api-auth
|
|
2
2
|
|
|
3
|
+
## 12.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f78740d: Remove Unicode dash punctuation from package prose and source comments.
|
|
8
|
+
- 9ebc5b4: Add an authenticated, rate-limited workspace slug availability endpoint.
|
|
9
|
+
- Updated dependencies [f78740d]
|
|
10
|
+
- Updated dependencies [94aba88]
|
|
11
|
+
- Updated dependencies [f13e8bb]
|
|
12
|
+
- Updated dependencies [9ebc5b4]
|
|
13
|
+
- Updated dependencies [34a5639]
|
|
14
|
+
- Updated dependencies [e1efaee]
|
|
15
|
+
- @shipfox/api-auth-dto@12.0.0
|
|
16
|
+
- @shipfox/api-common-dto@12.0.0
|
|
17
|
+
- @shipfox/inter-module@0.2.3
|
|
18
|
+
- @shipfox/node-fastify@0.4.1
|
|
19
|
+
- @shipfox/node-module@1.0.5
|
|
20
|
+
- @shipfox/api-workspaces-dto@12.0.0
|
|
21
|
+
- @shipfox/node-jwt@0.4.0
|
|
22
|
+
- @shipfox/node-postgres@0.5.0
|
|
23
|
+
- @shipfox/node-rate-limit@0.4.0
|
|
24
|
+
- @shipfox/api-auth-context@12.0.0
|
|
25
|
+
- @shipfox/api-email-challenges@1.1.7
|
|
26
|
+
- @shipfox/node-drizzle@0.3.5
|
|
27
|
+
- @shipfox/node-outbox@0.2.6
|
|
28
|
+
|
|
3
29
|
## 11.0.0
|
|
4
30
|
|
|
5
31
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Required environment:
|
|
|
60
60
|
| `AUTH_JOB_LEASE_TOKEN_EXPIRES_IN` | `90m` | Job lease token lifetime. |
|
|
61
61
|
| `AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN` | `1h` | Runner session token lifetime. |
|
|
62
62
|
| `AUTH_REFRESH_TOKEN_EXPIRES_IN_DAYS` | `14` | Refresh token and cookie lifetime. |
|
|
63
|
-
| `AUTH_REFRESH_ROTATION_GRACE_SECONDS` | `30` | Grace window for accepting a
|
|
63
|
+
| `AUTH_REFRESH_ROTATION_GRACE_SECONDS` | `30` | Grace window for accepting a recently rotated refresh token during concurrent refreshes. |
|
|
64
64
|
| `AUTH_REFRESH_COOKIE_NAME` | `shipfox_refresh_token` | HTTP cookie name for refresh sessions. |
|
|
65
65
|
| `AUTH_PASSWORD_ENABLED` | `true` | Enables password and email-verification routes. Set to `false` when another module provides login. Server construction fails if no login method is available. |
|
|
66
66
|
| `AUTH_SIGNUP_GATE_ENABLED` | `false` | Restricts new account creation to the configured signup email allowlist. Requires `AUTH_SIGNUP_ALLOWED_EMAIL_DOMAINS` or `AUTH_SIGNUP_ALLOWED_EMAILS` to be set. |
|
|
@@ -116,16 +116,16 @@ itself.
|
|
|
116
116
|
- **Audience:** signed-in users on first-party clients (web app, CLI).
|
|
117
117
|
- **Grants:** the user's own account routes and, downstream, any route that
|
|
118
118
|
authorizes against the membership claims. Its scope is "this user, with these
|
|
119
|
-
memberships"
|
|
119
|
+
memberships", never a single resource.
|
|
120
120
|
- **Lifecycle:**
|
|
121
|
-
- *Emit
|
|
122
|
-
- *Exchange
|
|
121
|
+
- *Emit*: issued on login, signup verification, and password reset.
|
|
122
|
+
- *Exchange*: sent on each request; a longer-lived refresh session mints a
|
|
123
123
|
fresh token when it expires.
|
|
124
|
-
- *Store
|
|
124
|
+
- *Store*: held in client memory only, never persisted to disk or local
|
|
125
125
|
storage. Refresh sessions are stored server-side as hashes, never in raw form.
|
|
126
|
-
- *Discard
|
|
126
|
+
- *Discard*: expires at the configured `exp` time. Revoking the refresh
|
|
127
127
|
session blocks renewal but does not invalidate the issued access token.
|
|
128
|
-
- **Tradeoff
|
|
128
|
+
- **Tradeoff: stale memberships:** because memberships ride in the claims, a
|
|
129
129
|
membership or workspace-status change only takes effect on the next token
|
|
130
130
|
issuance or refresh. Accepted because the recommended lifetime is 15 minutes,
|
|
131
131
|
so the staleness window is bounded and a per-request membership read is avoided.
|
|
@@ -137,7 +137,7 @@ The runner exchanges its long-lived registration token at startup, then uses the
|
|
|
137
137
|
short-lived session token to claim jobs. Heartbeat and step/report/log operations
|
|
138
138
|
use the per-job lease token instead.
|
|
139
139
|
|
|
140
|
-
- **Scope
|
|
140
|
+
- **Scope: one workspace's runner data plane.** The token names one runner
|
|
141
141
|
session, one workspace, the fixed `workspace` scope, and the session's immutable
|
|
142
142
|
label set. It can claim jobs for that workspace. It is not a user identity and
|
|
143
143
|
does not authorize dashboard management routes.
|
|
@@ -148,7 +148,7 @@ use the per-job lease token instead.
|
|
|
148
148
|
- **Mechanics.** Signed with HMAC-SHA256 using the derived runner-session key
|
|
149
149
|
and the `runner-session` audience. The default lifetime is `1h`, short enough
|
|
150
150
|
to bound the residual claim window when a registration token is revoked.
|
|
151
|
-
- **Tradeoff
|
|
151
|
+
- **Tradeoff: no per-session revocation in v1.** Claim stays stateless and does
|
|
152
152
|
not check the runner session row on each poll. Revoking the registration token
|
|
153
153
|
blocks new sessions, but an existing session can keep claiming until
|
|
154
154
|
`AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN` elapses. A job execution claimed before
|
|
@@ -163,7 +163,7 @@ A single-job **capability** token, not a session. It is the means by which a
|
|
|
163
163
|
runner proves it is the legitimate holder of one specific job while it reports
|
|
164
164
|
progress and drives that job to completion.
|
|
165
165
|
|
|
166
|
-
- **Scope
|
|
166
|
+
- **Scope: exactly one job execution.** The token authorizes action on the
|
|
167
167
|
single job execution it names, for the runner holding it. It is **not** a runner
|
|
168
168
|
identity, it is **not** workspace-wide, and it is **not** a substitute for the
|
|
169
169
|
long-lived runner credential used to claim work in the first place. The
|
|
@@ -175,9 +175,9 @@ progress and drives that job to completion.
|
|
|
175
175
|
- **Trust boundary.** There is exactly one issuer: the scheduling side mints a
|
|
176
176
|
lease when a runner claims a job execution, next-step re-scopes it to the
|
|
177
177
|
dispatched step attempt, and heartbeat re-mints the same narrow capability
|
|
178
|
-
after server state accepts the heartbeat. Everything
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
after server state accepts the heartbeat. Everything downstream only *verifies*
|
|
179
|
+
the token with an in-process signature check. It makes no callback to the
|
|
180
|
+
issuer. The runner, and the untrusted agent workload it hosts, never mints
|
|
181
181
|
or modifies a token; it only presents the one it was handed.
|
|
182
182
|
- **Mechanics.** Signed with HMAC-SHA256 using the job-lease key derived from
|
|
183
183
|
`AUTH_ROOT_KEY`. Its claims name the job, job execution, and surrounding
|
|
@@ -187,13 +187,13 @@ progress and drives that job to completion.
|
|
|
187
187
|
execution. The root key is supplied through configuration, never embedded in
|
|
188
188
|
code or committed. The raw token must **never** be written to logs, traces, or
|
|
189
189
|
error payloads. There is no automatic redaction to fall back on.
|
|
190
|
-
- **Defense in depth
|
|
190
|
+
- **Defense in depth: server state is the final authority.** A valid token is
|
|
191
191
|
never sufficient on its own to advance work. On the lease's own request path the
|
|
192
192
|
gate is server-side step and progression state: a report against a step that has
|
|
193
193
|
already reached a terminal state (finished, failed, or cancelled) is ignored, so
|
|
194
194
|
a still-valid token cannot re-drive work that is already done. Job-level
|
|
195
195
|
finalization is enforced outside the lease path. Cancellation flows the other way
|
|
196
|
-
as well
|
|
196
|
+
as well: the server can ask the runner to stop at any point, and that request
|
|
197
197
|
rides on the response to each heartbeat rather than depending on the token.
|
|
198
198
|
- **Log append scope.** A step-scoped lease is a signed membership and attempt
|
|
199
199
|
check for append-log authorization. It is not an active-step proof: until the
|
|
@@ -207,7 +207,7 @@ progress and drives that job to completion.
|
|
|
207
207
|
other workspaces. If `AUTH_ROOT_KEY` leaks, rotate it. This invalidates every
|
|
208
208
|
live token and email challenge derived from the old root, including every live
|
|
209
209
|
lease, and forces fresh claims.
|
|
210
|
-
- **Tradeoff
|
|
210
|
+
- **Tradeoff: no per-token revocation.** A single outstanding lease cannot be
|
|
211
211
|
revoked on its own by token ID. The claiming runner's credential is not
|
|
212
212
|
re-checked on each request, so a lease for already-claimed work can continue to
|
|
213
213
|
be renewed by heartbeat until server state cancels, completes, times out, or
|
|
@@ -217,7 +217,7 @@ progress and drives that job to completion.
|
|
|
217
217
|
- **Guidelines for future changes.** Keep the authority narrow: do not add claims
|
|
218
218
|
that grant access beyond the single job, keep the lifetime bounded, keep a single
|
|
219
219
|
issuer, and keep every other side verify-only. If the no-revocation window ever
|
|
220
|
-
proves too wide, the right fix is to bind the lease to live runner or job state
|
|
220
|
+
proves too wide, the right fix is to bind the lease to live runner or job state,
|
|
221
221
|
not to broaden what the token itself can authorize.
|
|
222
222
|
|
|
223
223
|
## Routes
|
|
@@ -433,7 +433,7 @@ unauthorized` error when the verified request context is missing, inconsistent,
|
|
|
433
433
|
or has no refresh-session ID. It exposes neither refresh-token material nor
|
|
434
434
|
storage details.
|
|
435
435
|
|
|
436
|
-
## Data
|
|
436
|
+
## Data model
|
|
437
437
|
|
|
438
438
|
The module creates tables with the `auth_` prefix:
|
|
439
439
|
|
|
@@ -446,7 +446,7 @@ The module creates tables with the `auth_` prefix:
|
|
|
446
446
|
|
|
447
447
|
Passwords use Argon2id. Password reset tokens and refresh tokens are opaque tokens stored as hashes. Email verification uses the shared email-challenges module.
|
|
448
448
|
|
|
449
|
-
## Behavior
|
|
449
|
+
## Behavior notes
|
|
450
450
|
|
|
451
451
|
- Signup sends an eight-digit verification code and returns the new user with its challenge ID.
|
|
452
452
|
- Signup gating blocks account creation only. Existing users keep signing in, even when their address is not on the current allowlist.
|
|
@@ -6,7 +6,7 @@ export declare function jobLeaseParamsFrom(source: JobLeaseParamSource, stepScop
|
|
|
6
6
|
currentStepAttempt: number;
|
|
7
7
|
}): IssueJobLeaseTokenParams;
|
|
8
8
|
export declare function issueJobLeaseToken(claims: IssueJobLeaseTokenParams): Promise<string>;
|
|
9
|
-
/** Returns the claims on success, or `null` for any invalid input
|
|
9
|
+
/** Returns the claims on success, or `null` for any invalid input. Never throws. */
|
|
10
10
|
export declare function verifyJobLeaseToken(token: string): Promise<JobLeaseTokenClaims | null>;
|
|
11
11
|
export {};
|
|
12
12
|
//# sourceMappingURL=job-lease-token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"job-lease-token.d.ts","sourceRoot":"","sources":["../../src/core/job-lease-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,mBAAmB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;AAExF,KAAK,mBAAmB,GAAG,IAAI,CAC7B,mBAAmB,EACjB,eAAe,GACf,sBAAsB,GACtB,OAAO,GACP,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,iBAAiB,CACpB,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,mBAAmB,EAC3B,SAAS,CAAC,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAC,GAC9D,wBAAwB,CAW1B;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B1F;AAED,
|
|
1
|
+
{"version":3,"file":"job-lease-token.d.ts","sourceRoot":"","sources":["../../src/core/job-lease-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,uBAAuB,CAAC;AAO/B,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,mBAAmB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;AAExF,KAAK,mBAAmB,GAAG,IAAI,CAC7B,mBAAmB,EACjB,eAAe,GACf,sBAAsB,GACtB,OAAO,GACP,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,iBAAiB,CACpB,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,mBAAmB,EAC3B,SAAS,CAAC,EAAE;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAC,GAC9D,wBAAwB,CAW1B;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0B1F;AAED,oFAAoF;AACpF,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAc5F"}
|
|
@@ -40,7 +40,7 @@ export async function issueJobLeaseToken(claims) {
|
|
|
40
40
|
recordTokenIssued('job_lease');
|
|
41
41
|
return token;
|
|
42
42
|
}
|
|
43
|
-
/** Returns the claims on success, or `null` for any invalid input
|
|
43
|
+
/** Returns the claims on success, or `null` for any invalid input. Never throws. */ export async function verifyJobLeaseToken(token) {
|
|
44
44
|
try {
|
|
45
45
|
const claims = await verifyHs256({
|
|
46
46
|
token,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/job-lease-token.ts"],"sourcesContent":["import {\n JOB_LEASE_TOKEN_AUDIENCE,\n type JobLeaseTokenClaims,\n jobLeaseTokenClaimsSchema,\n} from '@shipfox/api-auth-dto';\nimport {jobLeaseTokenKey} from '@shipfox/node-auth-root-key';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {config} from '#config.js';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\n// `aud`, `iat` and `exp` are set by the codec (jose); callers supply the business ids only.\nexport type IssueJobLeaseTokenParams = Omit<JobLeaseTokenClaims, 'aud' | 'iat' | 'exp'>;\n\ntype JobLeaseParamSource = Pick<\n JobLeaseTokenClaims,\n | 'workflowRunId'\n | 'workflowRunAttemptId'\n | 'jobId'\n | 'jobExecutionId'\n | 'projectId'\n | 'workspaceId'\n | 'runnerSessionId'\n>;\n\nexport function jobLeaseParamsFrom(\n source: JobLeaseParamSource,\n stepScope?: {currentStepId: string; currentStepAttempt: number},\n): IssueJobLeaseTokenParams {\n return {\n workflowRunId: source.workflowRunId,\n workflowRunAttemptId: source.workflowRunAttemptId,\n jobId: source.jobId,\n jobExecutionId: source.jobExecutionId,\n projectId: source.projectId,\n workspaceId: source.workspaceId,\n runnerSessionId: source.runnerSessionId,\n ...(stepScope ? stepScope : {}),\n };\n}\n\nexport async function issueJobLeaseToken(claims: IssueJobLeaseTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n workflowRunId: claims.workflowRunId,\n ...(claims.workflowRunAttempt === undefined\n ? {}\n : {workflowRunAttempt: claims.workflowRunAttempt}),\n workflowRunAttemptId: claims.workflowRunAttemptId,\n jobId: claims.jobId,\n jobExecutionId: claims.jobExecutionId,\n projectId: claims.projectId,\n workspaceId: claims.workspaceId,\n runnerSessionId: claims.runnerSessionId,\n ...(claims.currentStepId && claims.currentStepAttempt !== undefined\n ? {\n currentStepId: claims.currentStepId,\n currentStepAttempt: claims.currentStepAttempt,\n }\n : {}),\n },\n secret: jobLeaseTokenKey(),\n expiresIn: config.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN,\n audience: JOB_LEASE_TOKEN_AUDIENCE,\n });\n recordTokenIssued('job_lease');\n return token;\n}\n\n/** Returns the claims on success, or `null` for any invalid input
|
|
1
|
+
{"version":3,"sources":["../../src/core/job-lease-token.ts"],"sourcesContent":["import {\n JOB_LEASE_TOKEN_AUDIENCE,\n type JobLeaseTokenClaims,\n jobLeaseTokenClaimsSchema,\n} from '@shipfox/api-auth-dto';\nimport {jobLeaseTokenKey} from '@shipfox/node-auth-root-key';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {config} from '#config.js';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\n// `aud`, `iat` and `exp` are set by the codec (jose); callers supply the business ids only.\nexport type IssueJobLeaseTokenParams = Omit<JobLeaseTokenClaims, 'aud' | 'iat' | 'exp'>;\n\ntype JobLeaseParamSource = Pick<\n JobLeaseTokenClaims,\n | 'workflowRunId'\n | 'workflowRunAttemptId'\n | 'jobId'\n | 'jobExecutionId'\n | 'projectId'\n | 'workspaceId'\n | 'runnerSessionId'\n>;\n\nexport function jobLeaseParamsFrom(\n source: JobLeaseParamSource,\n stepScope?: {currentStepId: string; currentStepAttempt: number},\n): IssueJobLeaseTokenParams {\n return {\n workflowRunId: source.workflowRunId,\n workflowRunAttemptId: source.workflowRunAttemptId,\n jobId: source.jobId,\n jobExecutionId: source.jobExecutionId,\n projectId: source.projectId,\n workspaceId: source.workspaceId,\n runnerSessionId: source.runnerSessionId,\n ...(stepScope ? stepScope : {}),\n };\n}\n\nexport async function issueJobLeaseToken(claims: IssueJobLeaseTokenParams): Promise<string> {\n const token = await signHs256({\n payload: {\n workflowRunId: claims.workflowRunId,\n ...(claims.workflowRunAttempt === undefined\n ? {}\n : {workflowRunAttempt: claims.workflowRunAttempt}),\n workflowRunAttemptId: claims.workflowRunAttemptId,\n jobId: claims.jobId,\n jobExecutionId: claims.jobExecutionId,\n projectId: claims.projectId,\n workspaceId: claims.workspaceId,\n runnerSessionId: claims.runnerSessionId,\n ...(claims.currentStepId && claims.currentStepAttempt !== undefined\n ? {\n currentStepId: claims.currentStepId,\n currentStepAttempt: claims.currentStepAttempt,\n }\n : {}),\n },\n secret: jobLeaseTokenKey(),\n expiresIn: config.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN,\n audience: JOB_LEASE_TOKEN_AUDIENCE,\n });\n recordTokenIssued('job_lease');\n return token;\n}\n\n/** Returns the claims on success, or `null` for any invalid input. Never throws. */\nexport async function verifyJobLeaseToken(token: string): Promise<JobLeaseTokenClaims | null> {\n try {\n const claims = await verifyHs256({\n token,\n secret: jobLeaseTokenKey(),\n schema: jobLeaseTokenClaimsSchema,\n audience: JOB_LEASE_TOKEN_AUDIENCE,\n });\n recordTokenVerified('job_lease', 'ok');\n return claims;\n } catch {\n recordTokenVerified('job_lease', 'rejected');\n return null;\n }\n}\n"],"names":["JOB_LEASE_TOKEN_AUDIENCE","jobLeaseTokenClaimsSchema","jobLeaseTokenKey","signHs256","verifyHs256","config","recordTokenIssued","recordTokenVerified","jobLeaseParamsFrom","source","stepScope","workflowRunId","workflowRunAttemptId","jobId","jobExecutionId","projectId","workspaceId","runnerSessionId","issueJobLeaseToken","claims","token","payload","workflowRunAttempt","undefined","currentStepId","currentStepAttempt","secret","expiresIn","AUTH_JOB_LEASE_TOKEN_EXPIRES_IN","audience","verifyJobLeaseToken","schema"],"mappings":"AAAA,SACEA,wBAAwB,EAExBC,yBAAyB,QACpB,wBAAwB;AAC/B,SAAQC,gBAAgB,QAAO,8BAA8B;AAC7D,SAAQC,SAAS,EAAEC,WAAW,QAAO,oBAAoB;AACzD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,iBAAiB,EAAEC,mBAAmB,QAAO,oBAAoB;AAgBzE,OAAO,SAASC,mBACdC,MAA2B,EAC3BC,SAA+D;IAE/D,OAAO;QACLC,eAAeF,OAAOE,aAAa;QACnCC,sBAAsBH,OAAOG,oBAAoB;QACjDC,OAAOJ,OAAOI,KAAK;QACnBC,gBAAgBL,OAAOK,cAAc;QACrCC,WAAWN,OAAOM,SAAS;QAC3BC,aAAaP,OAAOO,WAAW;QAC/BC,iBAAiBR,OAAOQ,eAAe;QACvC,GAAIP,YAAYA,YAAY,CAAC,CAAC;IAChC;AACF;AAEA,OAAO,eAAeQ,mBAAmBC,MAAgC;IACvE,MAAMC,QAAQ,MAAMjB,UAAU;QAC5BkB,SAAS;YACPV,eAAeQ,OAAOR,aAAa;YACnC,GAAIQ,OAAOG,kBAAkB,KAAKC,YAC9B,CAAC,IACD;gBAACD,oBAAoBH,OAAOG,kBAAkB;YAAA,CAAC;YACnDV,sBAAsBO,OAAOP,oBAAoB;YACjDC,OAAOM,OAAON,KAAK;YACnBC,gBAAgBK,OAAOL,cAAc;YACrCC,WAAWI,OAAOJ,SAAS;YAC3BC,aAAaG,OAAOH,WAAW;YAC/BC,iBAAiBE,OAAOF,eAAe;YACvC,GAAIE,OAAOK,aAAa,IAAIL,OAAOM,kBAAkB,KAAKF,YACtD;gBACEC,eAAeL,OAAOK,aAAa;gBACnCC,oBAAoBN,OAAOM,kBAAkB;YAC/C,IACA,CAAC,CAAC;QACR;QACAC,QAAQxB;QACRyB,WAAWtB,OAAOuB,+BAA+B;QACjDC,UAAU7B;IACZ;IACAM,kBAAkB;IAClB,OAAOc;AACT;AAEA,kFAAkF,GAClF,OAAO,eAAeU,oBAAoBV,KAAa;IACrD,IAAI;QACF,MAAMD,SAAS,MAAMf,YAAY;YAC/BgB;YACAM,QAAQxB;YACR6B,QAAQ9B;YACR4B,UAAU7B;QACZ;QACAO,oBAAoB,aAAa;QACjC,OAAOY;IACT,EAAE,OAAM;QACNZ,oBAAoB,aAAa;QACjC,OAAO;IACT;AACF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RunnerSessionTokenClaims } from '@shipfox/api-auth-dto';
|
|
2
2
|
export type IssueRunnerSessionTokenParams = Omit<RunnerSessionTokenClaims, 'aud' | 'iat' | 'exp'>;
|
|
3
3
|
export declare function issueRunnerSessionToken(claims: IssueRunnerSessionTokenParams): Promise<string>;
|
|
4
|
-
/** Returns the claims on success, or `null` for any invalid input
|
|
4
|
+
/** Returns the claims on success, or `null` for any invalid input. Never throws. */
|
|
5
5
|
export declare function verifyRunnerSessionToken(token: string): Promise<RunnerSessionTokenClaims | null>;
|
|
6
6
|
//# sourceMappingURL=runner-session-token.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner-session-token.d.ts","sourceRoot":"","sources":["../../src/core/runner-session-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,wBAAwB,EAE9B,MAAM,uBAAuB,CAAC;AAO/B,MAAM,MAAM,6BAA6B,GAAG,IAAI,CAAC,wBAAwB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;AAElG,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,
|
|
1
|
+
{"version":3,"file":"runner-session-token.d.ts","sourceRoot":"","sources":["../../src/core/runner-session-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,wBAAwB,EAE9B,MAAM,uBAAuB,CAAC;AAO/B,MAAM,MAAM,6BAA6B,GAAG,IAAI,CAAC,wBAAwB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC;AAElG,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,oFAAoF;AACpF,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC,CAc1C"}
|
|
@@ -19,7 +19,7 @@ export async function issueRunnerSessionToken(claims) {
|
|
|
19
19
|
recordTokenIssued('runner_session');
|
|
20
20
|
return token;
|
|
21
21
|
}
|
|
22
|
-
/** Returns the claims on success, or `null` for any invalid input
|
|
22
|
+
/** Returns the claims on success, or `null` for any invalid input. Never throws. */ export async function verifyRunnerSessionToken(token) {
|
|
23
23
|
try {
|
|
24
24
|
const claims = await verifyHs256({
|
|
25
25
|
token,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/runner-session-token.ts"],"sourcesContent":["import {\n RUNNER_SESSION_TOKEN_AUDIENCE,\n type RunnerSessionTokenClaims,\n runnerSessionTokenClaimsSchema,\n} from '@shipfox/api-auth-dto';\nimport {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {config} from '#config.js';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\n// `aud`, `iat` and `exp` are set by the codec (jose); callers supply the business ids only.\nexport type IssueRunnerSessionTokenParams = Omit<RunnerSessionTokenClaims, 'aud' | 'iat' | 'exp'>;\n\nexport async function issueRunnerSessionToken(\n claims: IssueRunnerSessionTokenParams,\n): Promise<string> {\n const token = await signHs256({\n payload: {\n runnerSessionId: claims.runnerSessionId,\n workspaceId: claims.workspaceId,\n scope: claims.scope,\n labels: claims.labels,\n maxClaims: claims.maxClaims,\n },\n secret: runnerSessionTokenKey(),\n expiresIn: config.AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN,\n audience: RUNNER_SESSION_TOKEN_AUDIENCE,\n });\n recordTokenIssued('runner_session');\n return token;\n}\n\n/** Returns the claims on success, or `null` for any invalid input
|
|
1
|
+
{"version":3,"sources":["../../src/core/runner-session-token.ts"],"sourcesContent":["import {\n RUNNER_SESSION_TOKEN_AUDIENCE,\n type RunnerSessionTokenClaims,\n runnerSessionTokenClaimsSchema,\n} from '@shipfox/api-auth-dto';\nimport {runnerSessionTokenKey} from '@shipfox/node-auth-root-key';\nimport {signHs256, verifyHs256} from '@shipfox/node-jwt';\nimport {config} from '#config.js';\nimport {recordTokenIssued, recordTokenVerified} from '#metrics/index.js';\n\n// `aud`, `iat` and `exp` are set by the codec (jose); callers supply the business ids only.\nexport type IssueRunnerSessionTokenParams = Omit<RunnerSessionTokenClaims, 'aud' | 'iat' | 'exp'>;\n\nexport async function issueRunnerSessionToken(\n claims: IssueRunnerSessionTokenParams,\n): Promise<string> {\n const token = await signHs256({\n payload: {\n runnerSessionId: claims.runnerSessionId,\n workspaceId: claims.workspaceId,\n scope: claims.scope,\n labels: claims.labels,\n maxClaims: claims.maxClaims,\n },\n secret: runnerSessionTokenKey(),\n expiresIn: config.AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN,\n audience: RUNNER_SESSION_TOKEN_AUDIENCE,\n });\n recordTokenIssued('runner_session');\n return token;\n}\n\n/** Returns the claims on success, or `null` for any invalid input. Never throws. */\nexport async function verifyRunnerSessionToken(\n token: string,\n): Promise<RunnerSessionTokenClaims | null> {\n try {\n const claims = await verifyHs256({\n token,\n secret: runnerSessionTokenKey(),\n schema: runnerSessionTokenClaimsSchema,\n audience: RUNNER_SESSION_TOKEN_AUDIENCE,\n });\n recordTokenVerified('runner_session', 'ok');\n return claims;\n } catch {\n recordTokenVerified('runner_session', 'rejected');\n return null;\n }\n}\n"],"names":["RUNNER_SESSION_TOKEN_AUDIENCE","runnerSessionTokenClaimsSchema","runnerSessionTokenKey","signHs256","verifyHs256","config","recordTokenIssued","recordTokenVerified","issueRunnerSessionToken","claims","token","payload","runnerSessionId","workspaceId","scope","labels","maxClaims","secret","expiresIn","AUTH_RUNNER_SESSION_TOKEN_EXPIRES_IN","audience","verifyRunnerSessionToken","schema"],"mappings":"AAAA,SACEA,6BAA6B,EAE7BC,8BAA8B,QACzB,wBAAwB;AAC/B,SAAQC,qBAAqB,QAAO,8BAA8B;AAClE,SAAQC,SAAS,EAAEC,WAAW,QAAO,oBAAoB;AACzD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,iBAAiB,EAAEC,mBAAmB,QAAO,oBAAoB;AAKzE,OAAO,eAAeC,wBACpBC,MAAqC;IAErC,MAAMC,QAAQ,MAAMP,UAAU;QAC5BQ,SAAS;YACPC,iBAAiBH,OAAOG,eAAe;YACvCC,aAAaJ,OAAOI,WAAW;YAC/BC,OAAOL,OAAOK,KAAK;YACnBC,QAAQN,OAAOM,MAAM;YACrBC,WAAWP,OAAOO,SAAS;QAC7B;QACAC,QAAQf;QACRgB,WAAWb,OAAOc,oCAAoC;QACtDC,UAAUpB;IACZ;IACAM,kBAAkB;IAClB,OAAOI;AACT;AAEA,kFAAkF,GAClF,OAAO,eAAeW,yBACpBX,KAAa;IAEb,IAAI;QACF,MAAMD,SAAS,MAAML,YAAY;YAC/BM;YACAO,QAAQf;YACRoB,QAAQrB;YACRmB,UAAUpB;QACZ;QACAO,oBAAoB,kBAAkB;QACtC,OAAOE;IACT,EAAE,OAAM;QACNF,oBAAoB,kBAAkB;QACtC,OAAO;IACT;AACF"}
|
package/dist/db/rate-limits.d.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
expiresAt: Date;
|
|
7
|
-
timeoutMs: number;
|
|
8
|
-
}
|
|
9
|
-
export interface ConsumeAuthRateLimitResult {
|
|
10
|
-
count: number;
|
|
11
|
-
expiresAt: Date;
|
|
12
|
-
}
|
|
13
|
-
export declare function consumeAuthRateLimit(params: ConsumeAuthRateLimitParams): Promise<ConsumeAuthRateLimitResult>;
|
|
14
|
-
export declare function pruneExpiredAuthRateLimits(params?: {
|
|
1
|
+
import { type ConsumeRateLimitParams, type ConsumeRateLimitResult } from '@shipfox/node-rate-limit';
|
|
2
|
+
export type ConsumeAuthRateLimitParams = ConsumeRateLimitParams<string, string>;
|
|
3
|
+
export type ConsumeAuthRateLimitResult = ConsumeRateLimitResult;
|
|
4
|
+
export declare const consumeAuthRateLimit: (params: ConsumeRateLimitParams<string, string>) => Promise<ConsumeRateLimitResult>;
|
|
5
|
+
export declare const pruneExpiredAuthRateLimits: (params?: {
|
|
15
6
|
now?: Date | undefined;
|
|
16
7
|
minIntervalMs?: number | undefined;
|
|
17
|
-
})
|
|
8
|
+
}) => Promise<number | undefined>;
|
|
18
9
|
//# sourceMappingURL=rate-limits.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limits.d.ts","sourceRoot":"","sources":["../../src/db/rate-limits.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rate-limits.d.ts","sourceRoot":"","sources":["../../src/db/rate-limits.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAE5B,MAAM,0BAA0B,CAAC;AA+ClC,MAAM,MAAM,0BAA0B,GAAG,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAChF,MAAM,MAAM,0BAA0B,GAAG,sBAAsB,CAAC;AAChE,eAAO,MAAM,oBAAoB,qFAAsB,CAAC;AACxD,eAAO,MAAM,0BAA0B;;;iCAAoB,CAAC"}
|
package/dist/db/rate-limits.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { createRateLimitPersistence } from '@shipfox/node-rate-limit';
|
|
1
2
|
import { lt, sql } from 'drizzle-orm';
|
|
2
3
|
import { db } from './db.js';
|
|
3
4
|
import { authRateLimits } from './schema/rate-limits.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const persistence = createRateLimitPersistence({
|
|
6
|
+
transaction: (callback)=>db().transaction(callback),
|
|
7
|
+
setStatementTimeout: async (transaction, timeoutMs)=>{
|
|
8
|
+
await transaction.execute(sql`select set_config('statement_timeout', ${`${timeoutMs}ms`}, true)`);
|
|
9
|
+
},
|
|
10
|
+
consume: async (transaction, params)=>{
|
|
11
|
+
const rows = await transaction.insert(authRateLimits).values({
|
|
8
12
|
action: params.action,
|
|
9
13
|
scope: params.scope,
|
|
10
14
|
identifierHmac: params.identifierHmac,
|
|
@@ -26,19 +30,14 @@ export async function consumeAuthRateLimit(params) {
|
|
|
26
30
|
count: authRateLimits.count,
|
|
27
31
|
expiresAt: authRateLimits.expiresAt
|
|
28
32
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (minIntervalMs > 0 && now.getTime() < nextPruneAt) return undefined;
|
|
39
|
-
nextPruneAt = now.getTime() + minIntervalMs;
|
|
40
|
-
const result = await db().delete(authRateLimits).where(lt(authRateLimits.expiresAt, now));
|
|
41
|
-
return result.rowCount ?? 0;
|
|
42
|
-
}
|
|
33
|
+
return rows[0];
|
|
34
|
+
},
|
|
35
|
+
prune: async (now)=>{
|
|
36
|
+
const result = await db().delete(authRateLimits).where(lt(authRateLimits.expiresAt, now));
|
|
37
|
+
return result.rowCount ?? 0;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
export const consumeAuthRateLimit = persistence.consume;
|
|
41
|
+
export const pruneExpiredAuthRateLimits = persistence.prune;
|
|
43
42
|
|
|
44
43
|
//# sourceMappingURL=rate-limits.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/rate-limits.ts"],"sourcesContent":["import {lt, sql} from 'drizzle-orm';\nimport {db} from './db.js';\nimport {authRateLimits} from './schema/rate-limits.js';\n\
|
|
1
|
+
{"version":3,"sources":["../../src/db/rate-limits.ts"],"sourcesContent":["import {\n type ConsumeRateLimitParams,\n type ConsumeRateLimitResult,\n createRateLimitPersistence,\n} from '@shipfox/node-rate-limit';\nimport {lt, sql} from 'drizzle-orm';\nimport {db} from './db.js';\nimport {authRateLimits} from './schema/rate-limits.js';\n\ntype AuthRateLimitTransaction = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nconst persistence = createRateLimitPersistence<AuthRateLimitTransaction>({\n transaction: (callback) => db().transaction(callback),\n setStatementTimeout: async (transaction, timeoutMs) => {\n await transaction.execute(\n sql`select set_config('statement_timeout', ${`${timeoutMs}ms`}, true)`,\n );\n },\n consume: async (transaction, params) => {\n const rows = await transaction\n .insert(authRateLimits)\n .values({\n action: params.action,\n scope: params.scope,\n identifierHmac: params.identifierHmac,\n windowStart: params.windowStart,\n count: 1,\n expiresAt: params.expiresAt,\n })\n .onConflictDoUpdate({\n target: [\n authRateLimits.action,\n authRateLimits.scope,\n authRateLimits.identifierHmac,\n authRateLimits.windowStart,\n ],\n set: {\n count: sql`${authRateLimits.count} + 1`,\n updatedAt: sql`now()`,\n },\n })\n .returning({count: authRateLimits.count, expiresAt: authRateLimits.expiresAt});\n\n return rows[0];\n },\n prune: async (now) => {\n const result = await db().delete(authRateLimits).where(lt(authRateLimits.expiresAt, now));\n return result.rowCount ?? 0;\n },\n});\n\nexport type ConsumeAuthRateLimitParams = ConsumeRateLimitParams<string, string>;\nexport type ConsumeAuthRateLimitResult = ConsumeRateLimitResult;\nexport const consumeAuthRateLimit = persistence.consume;\nexport const pruneExpiredAuthRateLimits = persistence.prune;\n"],"names":["createRateLimitPersistence","lt","sql","db","authRateLimits","persistence","transaction","callback","setStatementTimeout","timeoutMs","execute","consume","params","rows","insert","values","action","scope","identifierHmac","windowStart","count","expiresAt","onConflictDoUpdate","target","set","updatedAt","returning","prune","now","result","delete","where","rowCount","consumeAuthRateLimit","pruneExpiredAuthRateLimits"],"mappings":"AAAA,SAGEA,0BAA0B,QACrB,2BAA2B;AAClC,SAAQC,EAAE,EAAEC,GAAG,QAAO,cAAc;AACpC,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,cAAc,QAAO,0BAA0B;AAIvD,MAAMC,cAAcL,2BAAqD;IACvEM,aAAa,CAACC,WAAaJ,KAAKG,WAAW,CAACC;IAC5CC,qBAAqB,OAAOF,aAAaG;QACvC,MAAMH,YAAYI,OAAO,CACvBR,GAAG,CAAC,uCAAuC,EAAE,GAAGO,UAAU,EAAE,CAAC,CAAC,OAAO,CAAC;IAE1E;IACAE,SAAS,OAAOL,aAAaM;QAC3B,MAAMC,OAAO,MAAMP,YAChBQ,MAAM,CAACV,gBACPW,MAAM,CAAC;YACNC,QAAQJ,OAAOI,MAAM;YACrBC,OAAOL,OAAOK,KAAK;YACnBC,gBAAgBN,OAAOM,cAAc;YACrCC,aAAaP,OAAOO,WAAW;YAC/BC,OAAO;YACPC,WAAWT,OAAOS,SAAS;QAC7B,GACCC,kBAAkB,CAAC;YAClBC,QAAQ;gBACNnB,eAAeY,MAAM;gBACrBZ,eAAea,KAAK;gBACpBb,eAAec,cAAc;gBAC7Bd,eAAee,WAAW;aAC3B;YACDK,KAAK;gBACHJ,OAAOlB,GAAG,CAAC,EAAEE,eAAegB,KAAK,CAAC,IAAI,CAAC;gBACvCK,WAAWvB,GAAG,CAAC,KAAK,CAAC;YACvB;QACF,GACCwB,SAAS,CAAC;YAACN,OAAOhB,eAAegB,KAAK;YAAEC,WAAWjB,eAAeiB,SAAS;QAAA;QAE9E,OAAOR,IAAI,CAAC,EAAE;IAChB;IACAc,OAAO,OAAOC;QACZ,MAAMC,SAAS,MAAM1B,KAAK2B,MAAM,CAAC1B,gBAAgB2B,KAAK,CAAC9B,GAAGG,eAAeiB,SAAS,EAAEO;QACpF,OAAOC,OAAOG,QAAQ,IAAI;IAC5B;AACF;AAIA,OAAO,MAAMC,uBAAuB5B,YAAYM,OAAO,CAAC;AACxD,OAAO,MAAMuB,6BAA6B7B,YAAYsB,KAAK,CAAC"}
|
|
@@ -11,7 +11,7 @@ export interface CreateRefreshTokenParams {
|
|
|
11
11
|
export declare function createRefreshToken(params: CreateRefreshTokenParams): Promise<RefreshToken>;
|
|
12
12
|
export declare function createRefreshTokenForActiveUser(params: CreateRefreshTokenParams): Promise<RefreshToken | undefined>;
|
|
13
13
|
/**
|
|
14
|
-
* Looks up the live session token by hash
|
|
14
|
+
* Looks up the live session token by hash. It can still authenticate as
|
|
15
15
|
* the current session. Returns `undefined` for revoked, expired, or already
|
|
16
16
|
* rotated tokens; rotated rows survive in the table only for the grace window
|
|
17
17
|
* and are not "active".
|
|
@@ -22,8 +22,8 @@ export declare function findActiveRefreshTokenByHash(params: {
|
|
|
22
22
|
/**
|
|
23
23
|
* Looks up a non-revoked, non-expired token by hash, including ones already
|
|
24
24
|
* rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to
|
|
25
|
-
* rotate, honour the grace window, or reject reuse, so
|
|
26
|
-
* {@link findActiveRefreshTokenByHash}
|
|
25
|
+
* rotate, honour the grace window, or reject reuse, so, unlike
|
|
26
|
+
* {@link findActiveRefreshTokenByHash}, it must still see rotated rows.
|
|
27
27
|
*/
|
|
28
28
|
export declare function findRefreshTokenByHash(params: {
|
|
29
29
|
hashedToken: string;
|
|
@@ -35,7 +35,7 @@ export async function createRefreshTokenForActiveUser(params) {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* Looks up the live session token by hash
|
|
38
|
+
* Looks up the live session token by hash. It can still authenticate as
|
|
39
39
|
* the current session. Returns `undefined` for revoked, expired, or already
|
|
40
40
|
* rotated tokens; rotated rows survive in the table only for the grace window
|
|
41
41
|
* and are not "active".
|
|
@@ -48,8 +48,8 @@ export async function createRefreshTokenForActiveUser(params) {
|
|
|
48
48
|
/**
|
|
49
49
|
* Looks up a non-revoked, non-expired token by hash, including ones already
|
|
50
50
|
* rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to
|
|
51
|
-
* rotate, honour the grace window, or reject reuse, so
|
|
52
|
-
* {@link findActiveRefreshTokenByHash}
|
|
51
|
+
* rotate, honour the grace window, or reject reuse, so, unlike
|
|
52
|
+
* {@link findActiveRefreshTokenByHash}, it must still see rotated rows.
|
|
53
53
|
*/ export async function findRefreshTokenByHash(params) {
|
|
54
54
|
const rows = await db().select().from(refreshTokens).where(and(eq(refreshTokens.hashedToken, params.hashedToken), isNull(refreshTokens.revokedAt), gt(refreshTokens.expiresAt, sql`now()`))).limit(1);
|
|
55
55
|
const row = rows[0];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/refresh-tokens.ts"],"sourcesContent":["import {and, eq, gt, isNull, ne, sql} from 'drizzle-orm';\nimport type {RefreshToken} from '#core/entities/refresh-token.js';\nimport {db} from './db.js';\nimport {refreshTokens, toRefreshToken} from './schema/refresh-tokens.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport async function lockUserSessionMutations(tx: Tx, userId: string): Promise<void> {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`auth_user_sessions:${userId}`}))`);\n}\n\nexport interface CreateRefreshTokenParams {\n sessionId?: string | undefined;\n userId: string;\n hashedToken: string;\n expiresAt: Date;\n}\n\nexport async function createRefreshToken(params: CreateRefreshTokenParams): Promise<RefreshToken> {\n const rows = await db()\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n}\n\nexport async function createRefreshTokenForActiveUser(\n params: CreateRefreshTokenParams,\n): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n await lockUserSessionMutations(tx, params.userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n });\n}\n\n/**\n * Looks up the live session token by hash — one that can still authenticate as\n * the current session. Returns `undefined` for revoked, expired, or already\n * rotated tokens; rotated rows survive in the table only for the grace window\n * and are not \"active\".\n */\nexport async function findActiveRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Looks up a non-revoked, non-expired token by hash, including ones already\n * rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to\n * rotate, honour the grace window, or reject reuse, so — unlike\n * {@link findActiveRefreshTokenByHash} — it must still see rotated rows.\n */\nexport async function findRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Atomically claims rotation and inserts the successor token. The predecessor is\n * marked rather than deleted so later reuse of the retired token can be detected.\n * If the successor insert fails, the predecessor rotation rolls back.\n */\nexport async function rotateRefreshToken(params: {\n userId?: string | undefined;\n id: string;\n currentHashedToken: string;\n nextHashedToken: string;\n expiresAt: Date;\n}): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n const userId =\n params.userId ??\n (\n await tx\n .select({userId: refreshTokens.userId})\n .from(refreshTokens)\n .where(eq(refreshTokens.id, params.id))\n .limit(1)\n )[0]?.userId;\n if (!userId) return undefined;\n\n await lockUserSessionMutations(tx, userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rotatedRows = await tx\n .update(refreshTokens)\n .set({\n rotatedAt: sql`now()`,\n lastUsedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n and(\n eq(refreshTokens.id, params.id),\n eq(refreshTokens.hashedToken, params.currentHashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .returning();\n\n const rotated = rotatedRows[0];\n if (!rotated) return undefined;\n\n const successorRows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: rotated.sessionId,\n userId: rotated.userId,\n hashedToken: params.nextHashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n if (!successorRows[0]) throw new Error('Insert returned no rows');\n\n return toRefreshToken(rotated);\n });\n}\n\nexport async function revokeRefreshTokenByHash(params: {hashedToken: string}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(and(eq(refreshTokens.hashedToken, params.hashedToken), isNull(refreshTokens.revokedAt)));\n}\n\nexport async function revokeRefreshSession(params: {\n sessionId: string;\n userId: string;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.sessionId, params.sessionId),\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n ),\n );\n}\n\nexport async function revokeRefreshTokensForUser(params: {\n userId: string;\n exceptRefreshTokenId?: string | undefined;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n params.exceptRefreshTokenId ? ne(refreshTokens.id, params.exceptRefreshTokenId) : undefined,\n ),\n );\n}\n"],"names":["and","eq","gt","isNull","ne","sql","db","refreshTokens","toRefreshToken","users","lockUserSessionMutations","tx","userId","execute","createRefreshToken","params","rows","insert","values","sessionId","hashedToken","expiresAt","returning","row","Error","createRefreshTokenForActiveUser","transaction","userRows","select","status","from","where","id","limit","undefined","findActiveRefreshTokenByHash","revokedAt","rotatedAt","findRefreshTokenByHash","rotateRefreshToken","rotatedRows","update","set","lastUsedAt","updatedAt","currentHashedToken","rotated","successorRows","nextHashedToken","revokeRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","exceptRefreshTokenId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/refresh-tokens.ts"],"sourcesContent":["import {and, eq, gt, isNull, ne, sql} from 'drizzle-orm';\nimport type {RefreshToken} from '#core/entities/refresh-token.js';\nimport {db} from './db.js';\nimport {refreshTokens, toRefreshToken} from './schema/refresh-tokens.js';\nimport {users} from './schema/users.js';\n\ntype Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport async function lockUserSessionMutations(tx: Tx, userId: string): Promise<void> {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`auth_user_sessions:${userId}`}))`);\n}\n\nexport interface CreateRefreshTokenParams {\n sessionId?: string | undefined;\n userId: string;\n hashedToken: string;\n expiresAt: Date;\n}\n\nexport async function createRefreshToken(params: CreateRefreshTokenParams): Promise<RefreshToken> {\n const rows = await db()\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n}\n\nexport async function createRefreshTokenForActiveUser(\n params: CreateRefreshTokenParams,\n): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n await lockUserSessionMutations(tx, params.userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: params.sessionId,\n userId: params.userId,\n hashedToken: params.hashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toRefreshToken(row);\n });\n}\n\n/**\n * Looks up the live session token by hash. It can still authenticate as\n * the current session. Returns `undefined` for revoked, expired, or already\n * rotated tokens; rotated rows survive in the table only for the grace window\n * and are not \"active\".\n */\nexport async function findActiveRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Looks up a non-revoked, non-expired token by hash, including ones already\n * rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to\n * rotate, honour the grace window, or reject reuse, so, unlike\n * {@link findActiveRefreshTokenByHash}, it must still see rotated rows.\n */\nexport async function findRefreshTokenByHash(params: {\n hashedToken: string;\n}): Promise<RefreshToken | undefined> {\n const rows = await db()\n .select()\n .from(refreshTokens)\n .where(\n and(\n eq(refreshTokens.hashedToken, params.hashedToken),\n isNull(refreshTokens.revokedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .limit(1);\n\n const row = rows[0];\n if (!row) return undefined;\n return toRefreshToken(row);\n}\n\n/**\n * Atomically claims rotation and inserts the successor token. The predecessor is\n * marked rather than deleted so later reuse of the retired token can be detected.\n * If the successor insert fails, the predecessor rotation rolls back.\n */\nexport async function rotateRefreshToken(params: {\n userId?: string | undefined;\n id: string;\n currentHashedToken: string;\n nextHashedToken: string;\n expiresAt: Date;\n}): Promise<RefreshToken | undefined> {\n return await db().transaction(async (tx) => {\n const userId =\n params.userId ??\n (\n await tx\n .select({userId: refreshTokens.userId})\n .from(refreshTokens)\n .where(eq(refreshTokens.id, params.id))\n .limit(1)\n )[0]?.userId;\n if (!userId) return undefined;\n\n await lockUserSessionMutations(tx, userId);\n const userRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, userId))\n .limit(1);\n if (userRows[0]?.status !== 'active') return undefined;\n\n const rotatedRows = await tx\n .update(refreshTokens)\n .set({\n rotatedAt: sql`now()`,\n lastUsedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n and(\n eq(refreshTokens.id, params.id),\n eq(refreshTokens.hashedToken, params.currentHashedToken),\n isNull(refreshTokens.revokedAt),\n isNull(refreshTokens.rotatedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .returning();\n\n const rotated = rotatedRows[0];\n if (!rotated) return undefined;\n\n const successorRows = await tx\n .insert(refreshTokens)\n .values({\n sessionId: rotated.sessionId,\n userId: rotated.userId,\n hashedToken: params.nextHashedToken,\n expiresAt: params.expiresAt,\n })\n .returning();\n\n if (!successorRows[0]) throw new Error('Insert returned no rows');\n\n return toRefreshToken(rotated);\n });\n}\n\nexport async function revokeRefreshTokenByHash(params: {hashedToken: string}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(and(eq(refreshTokens.hashedToken, params.hashedToken), isNull(refreshTokens.revokedAt)));\n}\n\nexport async function revokeRefreshSession(params: {\n sessionId: string;\n userId: string;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.sessionId, params.sessionId),\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n ),\n );\n}\n\nexport async function revokeRefreshTokensForUser(params: {\n userId: string;\n exceptRefreshTokenId?: string | undefined;\n}): Promise<void> {\n await db()\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.userId, params.userId),\n isNull(refreshTokens.revokedAt),\n params.exceptRefreshTokenId ? ne(refreshTokens.id, params.exceptRefreshTokenId) : undefined,\n ),\n );\n}\n"],"names":["and","eq","gt","isNull","ne","sql","db","refreshTokens","toRefreshToken","users","lockUserSessionMutations","tx","userId","execute","createRefreshToken","params","rows","insert","values","sessionId","hashedToken","expiresAt","returning","row","Error","createRefreshTokenForActiveUser","transaction","userRows","select","status","from","where","id","limit","undefined","findActiveRefreshTokenByHash","revokedAt","rotatedAt","findRefreshTokenByHash","rotateRefreshToken","rotatedRows","update","set","lastUsedAt","updatedAt","currentHashedToken","rotated","successorRows","nextHashedToken","revokeRefreshTokenByHash","revokeRefreshSession","revokeRefreshTokensForUser","exceptRefreshTokenId"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AuthMethod } from '@shipfox/node-fastify';
|
|
2
2
|
/**
|
|
3
|
-
* Trust boundary: the signed token is the sole authority
|
|
3
|
+
* Trust boundary: the signed token is the sole authority. `claims.jobId` scopes
|
|
4
4
|
* every runner request to exactly one job. `workflowRunId`/`workflowRunAttemptId`/`workspaceId`
|
|
5
5
|
* are carried for consumers but are NOT verified against the database here.
|
|
6
6
|
*
|
|
@@ -2,7 +2,7 @@ import { AUTH_LEASED_JOB, setLeasedJobContext } from '@shipfox/api-auth-context'
|
|
|
2
2
|
import { verifyJobLeaseToken } from '#core/job-lease-token.js';
|
|
3
3
|
import { createBearerTokenAuthMethod } from './bearer-token-auth.js';
|
|
4
4
|
/**
|
|
5
|
-
* Trust boundary: the signed token is the sole authority
|
|
5
|
+
* Trust boundary: the signed token is the sole authority. `claims.jobId` scopes
|
|
6
6
|
* every runner request to exactly one job. `workflowRunId`/`workflowRunAttemptId`/`workspaceId`
|
|
7
7
|
* are carried for consumers but are NOT verified against the database here.
|
|
8
8
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/auth/lease-token-auth.ts"],"sourcesContent":["import {AUTH_LEASED_JOB, setLeasedJobContext} from '@shipfox/api-auth-context';\nimport type {AuthMethod} from '@shipfox/node-fastify';\nimport {verifyJobLeaseToken} from '#core/job-lease-token.js';\nimport {createBearerTokenAuthMethod} from './bearer-token-auth.js';\n\n/**\n * Trust boundary: the signed token is the sole authority
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/auth/lease-token-auth.ts"],"sourcesContent":["import {AUTH_LEASED_JOB, setLeasedJobContext} from '@shipfox/api-auth-context';\nimport type {AuthMethod} from '@shipfox/node-fastify';\nimport {verifyJobLeaseToken} from '#core/job-lease-token.js';\nimport {createBearerTokenAuthMethod} from './bearer-token-auth.js';\n\n/**\n * Trust boundary: the signed token is the sole authority. `claims.jobId` scopes\n * every runner request to exactly one job. `workflowRunId`/`workflowRunAttemptId`/`workspaceId`\n * are carried for consumers but are NOT verified against the database here.\n *\n * Revocation tradeoff: `runnerSessionId` is carried in the claims but is not checked\n * against runner-session storage, so a lease minted before its registration token is\n * revoked stays usable until it expires (`AUTH_JOB_LEASE_TOKEN_EXPIRES_IN`, 90m).\n * Accepted deliberately for a short-lived, job-scoped capability token; tightening\n * this would mean a per-request DB lookup on the hot status-reporting path. See the\n * \"Security model\" section in the package README for the full rationale.\n */\nexport function createLeaseTokenAuthMethod(): AuthMethod {\n return createBearerTokenAuthMethod({\n name: AUTH_LEASED_JOB,\n verifyToken: verifyJobLeaseToken,\n invalidTokenError: {message: 'Invalid or expired job lease token', code: 'unauthorized'},\n setContext: setLeasedJobContext,\n });\n}\n"],"names":["AUTH_LEASED_JOB","setLeasedJobContext","verifyJobLeaseToken","createBearerTokenAuthMethod","createLeaseTokenAuthMethod","name","verifyToken","invalidTokenError","message","code","setContext"],"mappings":"AAAA,SAAQA,eAAe,EAAEC,mBAAmB,QAAO,4BAA4B;AAE/E,SAAQC,mBAAmB,QAAO,2BAA2B;AAC7D,SAAQC,2BAA2B,QAAO,yBAAyB;AAEnE;;;;;;;;;;;CAWC,GACD,OAAO,SAASC;IACd,OAAOD,4BAA4B;QACjCE,MAAML;QACNM,aAAaJ;QACbK,mBAAmB;YAACC,SAAS;YAAsCC,MAAM;QAAc;QACvFC,YAAYT;IACd;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;AAyB7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgDD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,IACzD,SAAS,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,IAC3D,SAAS,cAAc,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ClientError } from '@shipfox/node-fastify';
|
|
2
|
-
import {
|
|
2
|
+
import { enforceRateLimit as enforceSharedRateLimit } from '@shipfox/node-rate-limit';
|
|
3
|
+
import { checkAuthRateLimit } from '#core/rate-limit.js';
|
|
3
4
|
const policies = {
|
|
4
5
|
login: {
|
|
5
6
|
ip: {
|
|
@@ -46,58 +47,36 @@ function routeName(request) {
|
|
|
46
47
|
async function enforceRateLimit(params) {
|
|
47
48
|
const policy = policies[params.action][params.scope];
|
|
48
49
|
if (!policy) return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
if (error instanceof AuthRateLimitUnavailableError) {
|
|
81
|
-
params.request.log.error({
|
|
82
|
-
action: error.action,
|
|
83
|
-
scope: error.scope,
|
|
84
|
-
route: routeName(params.request),
|
|
85
|
-
identifierHmacPrefix: error.identifierHmacPrefix,
|
|
86
|
-
err: error
|
|
87
|
-
}, 'Auth rate limiter unavailable');
|
|
88
|
-
throw new ClientError('Authentication rate limiter unavailable', 'auth-rate-limit-unavailable', {
|
|
89
|
-
status: 503,
|
|
90
|
-
data: {
|
|
91
|
-
action: error.action,
|
|
92
|
-
scope: error.scope,
|
|
93
|
-
route: routeName(params.request),
|
|
94
|
-
identifierHmacPrefix: error.identifierHmacPrefix
|
|
95
|
-
},
|
|
96
|
-
cause: error
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
50
|
+
await enforceSharedRateLimit({
|
|
51
|
+
request: params.request,
|
|
52
|
+
reply: params.reply,
|
|
53
|
+
check: ()=>checkAuthRateLimit({
|
|
54
|
+
action: params.action,
|
|
55
|
+
scope: params.scope,
|
|
56
|
+
identifier: params.identifier,
|
|
57
|
+
...policy
|
|
58
|
+
}),
|
|
59
|
+
route: routeName(params.request),
|
|
60
|
+
unavailableCode: 'auth-rate-limit-unavailable',
|
|
61
|
+
unavailableMessage: 'Authentication rate limiter unavailable',
|
|
62
|
+
setRetryAfter: (reply, retryAfterSeconds)=>{
|
|
63
|
+
reply.header('Retry-After', String(retryAfterSeconds));
|
|
64
|
+
},
|
|
65
|
+
logWarn: (request, context)=>{
|
|
66
|
+
request.log.warn(context, 'Auth rate limit blocked request');
|
|
67
|
+
},
|
|
68
|
+
logError: (request, context)=>{
|
|
69
|
+
request.log.error(context, 'Auth rate limiter unavailable');
|
|
70
|
+
},
|
|
71
|
+
createClientError: (presentation, cause)=>new ClientError(presentation.message, presentation.code, {
|
|
72
|
+
status: presentation.status,
|
|
73
|
+
...presentation.details ? {
|
|
74
|
+
details: presentation.details
|
|
75
|
+
} : {},
|
|
76
|
+
data: presentation.data,
|
|
77
|
+
cause
|
|
78
|
+
})
|
|
79
|
+
});
|
|
101
80
|
}
|
|
102
81
|
export function createAuthRateLimitPreHandler(action) {
|
|
103
82
|
return async (request, reply)=>{
|