@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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-auth",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "12.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -26,24 +26,24 @@
|
|
|
26
26
|
"@node-rs/argon2": "^2.0.2",
|
|
27
27
|
"drizzle-orm": "^0.45.2",
|
|
28
28
|
"zod": "^4.4.3",
|
|
29
|
-
"@shipfox/api-common-dto": "
|
|
30
|
-
"@shipfox/api-auth-context": "
|
|
31
|
-
"@shipfox/api-auth-dto": "
|
|
32
|
-
"@shipfox/api-email-challenges": "1.1.
|
|
33
|
-
"@shipfox/api-workspaces-dto": "
|
|
34
|
-
"@shipfox/inter-module": "0.2.
|
|
29
|
+
"@shipfox/api-common-dto": "12.0.0",
|
|
30
|
+
"@shipfox/api-auth-context": "12.0.0",
|
|
31
|
+
"@shipfox/api-auth-dto": "12.0.0",
|
|
32
|
+
"@shipfox/api-email-challenges": "1.1.7",
|
|
33
|
+
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
34
|
+
"@shipfox/inter-module": "0.2.3",
|
|
35
35
|
"@shipfox/config": "1.2.4",
|
|
36
|
-
"@shipfox/node-drizzle": "0.3.
|
|
36
|
+
"@shipfox/node-drizzle": "0.3.5",
|
|
37
37
|
"@shipfox/node-auth-root-key": "0.2.3",
|
|
38
|
-
"@shipfox/node-fastify": "0.4.
|
|
39
|
-
"@shipfox/node-jwt": "0.
|
|
38
|
+
"@shipfox/node-fastify": "0.4.1",
|
|
39
|
+
"@shipfox/node-jwt": "0.4.0",
|
|
40
40
|
"@shipfox/node-email": "0.3.4",
|
|
41
41
|
"@shipfox/node-mailer": "0.2.4",
|
|
42
|
-
"@shipfox/node-module": "1.0.
|
|
42
|
+
"@shipfox/node-module": "1.0.5",
|
|
43
43
|
"@shipfox/node-opentelemetry": "0.6.3",
|
|
44
44
|
"@shipfox/node-outbox": "0.2.6",
|
|
45
|
-
"@shipfox/node-postgres": "0.
|
|
46
|
-
"@shipfox/node-rate-limit": "0.
|
|
45
|
+
"@shipfox/node-postgres": "0.5.0",
|
|
46
|
+
"@shipfox/node-rate-limit": "0.4.0",
|
|
47
47
|
"@shipfox/node-tokens": "0.3.2"
|
|
48
48
|
},
|
|
49
49
|
"imports": {
|
|
@@ -66,7 +66,7 @@ export async function issueJobLeaseToken(claims: IssueJobLeaseTokenParams): Prom
|
|
|
66
66
|
return token;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Returns the claims on success, or `null` for any invalid input
|
|
69
|
+
/** Returns the claims on success, or `null` for any invalid input. Never throws. */
|
|
70
70
|
export async function verifyJobLeaseToken(token: string): Promise<JobLeaseTokenClaims | null> {
|
|
71
71
|
try {
|
|
72
72
|
const claims = await verifyHs256({
|
|
@@ -30,7 +30,7 @@ export async function issueRunnerSessionToken(
|
|
|
30
30
|
return token;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Returns the claims on success, or `null` for any invalid input
|
|
33
|
+
/** Returns the claims on success, or `null` for any invalid input. Never throws. */
|
|
34
34
|
export async function verifyRunnerSessionToken(
|
|
35
35
|
token: string,
|
|
36
36
|
): Promise<RunnerSessionTokenClaims | null> {
|
package/src/db/rate-limits.ts
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConsumeRateLimitParams,
|
|
3
|
+
type ConsumeRateLimitResult,
|
|
4
|
+
createRateLimitPersistence,
|
|
5
|
+
} from '@shipfox/node-rate-limit';
|
|
1
6
|
import {lt, sql} from 'drizzle-orm';
|
|
2
7
|
import {db} from './db.js';
|
|
3
8
|
import {authRateLimits} from './schema/rate-limits.js';
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
expiresAt: Date;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function consumeAuthRateLimit(
|
|
20
|
-
params: ConsumeAuthRateLimitParams,
|
|
21
|
-
): Promise<ConsumeAuthRateLimitResult> {
|
|
22
|
-
return await db().transaction(async (tx) => {
|
|
23
|
-
await tx.execute(sql`select set_config('statement_timeout', ${`${params.timeoutMs}ms`}, true)`);
|
|
24
|
-
|
|
25
|
-
const rows = await tx
|
|
10
|
+
type AuthRateLimitTransaction = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
|
|
11
|
+
|
|
12
|
+
const persistence = createRateLimitPersistence<AuthRateLimitTransaction>({
|
|
13
|
+
transaction: (callback) => db().transaction(callback),
|
|
14
|
+
setStatementTimeout: async (transaction, timeoutMs) => {
|
|
15
|
+
await transaction.execute(
|
|
16
|
+
sql`select set_config('statement_timeout', ${`${timeoutMs}ms`}, true)`,
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
consume: async (transaction, params) => {
|
|
20
|
+
const rows = await transaction
|
|
26
21
|
.insert(authRateLimits)
|
|
27
22
|
.values({
|
|
28
23
|
action: params.action,
|
|
@@ -46,24 +41,15 @@ export async function consumeAuthRateLimit(
|
|
|
46
41
|
})
|
|
47
42
|
.returning({count: authRateLimits.count, expiresAt: authRateLimits.expiresAt});
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const minIntervalMs = params.minIntervalMs ?? 60_000;
|
|
62
|
-
if (minIntervalMs > 0 && now.getTime() < nextPruneAt) return undefined;
|
|
63
|
-
|
|
64
|
-
nextPruneAt = now.getTime() + minIntervalMs;
|
|
65
|
-
|
|
66
|
-
const result = await db().delete(authRateLimits).where(lt(authRateLimits.expiresAt, now));
|
|
67
|
-
|
|
68
|
-
return result.rowCount ?? 0;
|
|
69
|
-
}
|
|
44
|
+
return rows[0];
|
|
45
|
+
},
|
|
46
|
+
prune: async (now) => {
|
|
47
|
+
const result = await db().delete(authRateLimits).where(lt(authRateLimits.expiresAt, now));
|
|
48
|
+
return result.rowCount ?? 0;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type ConsumeAuthRateLimitParams = ConsumeRateLimitParams<string, string>;
|
|
53
|
+
export type ConsumeAuthRateLimitResult = ConsumeRateLimitResult;
|
|
54
|
+
export const consumeAuthRateLimit = persistence.consume;
|
|
55
|
+
export const pruneExpiredAuthRateLimits = persistence.prune;
|
package/src/db/refresh-tokens.ts
CHANGED
|
@@ -61,7 +61,7 @@ export async function createRefreshTokenForActiveUser(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Looks up the live session token by hash
|
|
64
|
+
* Looks up the live session token by hash. It can still authenticate as
|
|
65
65
|
* the current session. Returns `undefined` for revoked, expired, or already
|
|
66
66
|
* rotated tokens; rotated rows survive in the table only for the grace window
|
|
67
67
|
* and are not "active".
|
|
@@ -90,8 +90,8 @@ export async function findActiveRefreshTokenByHash(params: {
|
|
|
90
90
|
/**
|
|
91
91
|
* Looks up a non-revoked, non-expired token by hash, including ones already
|
|
92
92
|
* rotated. Rotation reads {@link RefreshToken.rotatedAt} to decide whether to
|
|
93
|
-
* rotate, honour the grace window, or reject reuse, so
|
|
94
|
-
* {@link findActiveRefreshTokenByHash}
|
|
93
|
+
* rotate, honour the grace window, or reject reuse, so, unlike
|
|
94
|
+
* {@link findActiveRefreshTokenByHash}, it must still see rotated rows.
|
|
95
95
|
*/
|
|
96
96
|
export async function findRefreshTokenByHash(params: {
|
|
97
97
|
hashedToken: string;
|
|
@@ -4,7 +4,7 @@ import {verifyJobLeaseToken} from '#core/job-lease-token.js';
|
|
|
4
4
|
import {createBearerTokenAuthMethod} from './bearer-token-auth.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Trust boundary: the signed token is the sole authority
|
|
7
|
+
* Trust boundary: the signed token is the sole authority. `claims.jobId` scopes
|
|
8
8
|
* every runner request to exactly one job. `workflowRunId`/`workflowRunAttemptId`/`workspaceId`
|
|
9
9
|
* are carried for consumers but are NOT verified against the database here.
|
|
10
10
|
*
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';
|
|
2
|
+
import {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';
|
|
2
3
|
import {
|
|
3
4
|
type AuthRateLimitAction,
|
|
4
|
-
AuthRateLimitExceededError,
|
|
5
5
|
type AuthRateLimitPolicy,
|
|
6
6
|
type AuthRateLimitScope,
|
|
7
|
-
AuthRateLimitUnavailableError,
|
|
8
7
|
checkAuthRateLimit,
|
|
9
8
|
} from '#core/rate-limit.js';
|
|
10
9
|
|
|
@@ -49,68 +48,36 @@ async function enforceRateLimit(params: {
|
|
|
49
48
|
const policy = policies[params.action][params.scope];
|
|
50
49
|
if (!policy) return;
|
|
51
50
|
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (error instanceof AuthRateLimitUnavailableError) {
|
|
86
|
-
params.request.log.error(
|
|
87
|
-
{
|
|
88
|
-
action: error.action,
|
|
89
|
-
scope: error.scope,
|
|
90
|
-
route: routeName(params.request),
|
|
91
|
-
identifierHmacPrefix: error.identifierHmacPrefix,
|
|
92
|
-
err: error,
|
|
93
|
-
},
|
|
94
|
-
'Auth rate limiter unavailable',
|
|
95
|
-
);
|
|
96
|
-
throw new ClientError(
|
|
97
|
-
'Authentication rate limiter unavailable',
|
|
98
|
-
'auth-rate-limit-unavailable',
|
|
99
|
-
{
|
|
100
|
-
status: 503,
|
|
101
|
-
data: {
|
|
102
|
-
action: error.action,
|
|
103
|
-
scope: error.scope,
|
|
104
|
-
route: routeName(params.request),
|
|
105
|
-
identifierHmacPrefix: error.identifierHmacPrefix,
|
|
106
|
-
},
|
|
107
|
-
cause: error,
|
|
108
|
-
},
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
51
|
+
await enforceSharedRateLimit({
|
|
52
|
+
request: params.request,
|
|
53
|
+
reply: params.reply,
|
|
54
|
+
check: () =>
|
|
55
|
+
checkAuthRateLimit({
|
|
56
|
+
action: params.action,
|
|
57
|
+
scope: params.scope,
|
|
58
|
+
identifier: params.identifier,
|
|
59
|
+
...policy,
|
|
60
|
+
}),
|
|
61
|
+
route: routeName(params.request),
|
|
62
|
+
unavailableCode: 'auth-rate-limit-unavailable',
|
|
63
|
+
unavailableMessage: 'Authentication rate limiter unavailable',
|
|
64
|
+
setRetryAfter: (reply, retryAfterSeconds) => {
|
|
65
|
+
reply.header('Retry-After', String(retryAfterSeconds));
|
|
66
|
+
},
|
|
67
|
+
logWarn: (request, context) => {
|
|
68
|
+
request.log.warn(context, 'Auth rate limit blocked request');
|
|
69
|
+
},
|
|
70
|
+
logError: (request, context) => {
|
|
71
|
+
request.log.error(context, 'Auth rate limiter unavailable');
|
|
72
|
+
},
|
|
73
|
+
createClientError: (presentation, cause) =>
|
|
74
|
+
new ClientError(presentation.message, presentation.code, {
|
|
75
|
+
status: presentation.status,
|
|
76
|
+
...(presentation.details ? {details: presentation.details} : {}),
|
|
77
|
+
data: presentation.data,
|
|
78
|
+
cause,
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
114
81
|
}
|
|
115
82
|
|
|
116
83
|
export function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {
|