@shipfox/api-auth 19.0.0 → 20.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 +17 -2
- package/CHANGELOG.md +13 -0
- package/dist/core/cimd.d.ts +39 -0
- package/dist/core/cimd.d.ts.map +1 -0
- package/dist/core/cimd.js +392 -0
- package/dist/core/cimd.js.map +1 -0
- package/dist/core/errors.d.ts +14 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +26 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/oauth-client-resolver.d.ts +54 -0
- package/dist/core/oauth-client-resolver.d.ts.map +1 -0
- package/dist/core/oauth-client-resolver.js +154 -0
- package/dist/core/oauth-client-resolver.js.map +1 -0
- package/dist/core/oauth-client.d.ts +39 -0
- package/dist/core/oauth-client.d.ts.map +1 -0
- package/dist/core/oauth-client.js +236 -0
- package/dist/core/oauth-client.js.map +1 -0
- package/dist/db/agent-access-retention.d.ts +10 -0
- package/dist/db/agent-access-retention.d.ts.map +1 -0
- package/dist/db/agent-access-retention.js +11 -0
- package/dist/db/agent-access-retention.js.map +1 -0
- package/dist/db/agent-access.d.ts +100 -4
- package/dist/db/agent-access.d.ts.map +1 -1
- package/dist/db/agent-access.js +483 -45
- package/dist/db/agent-access.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/routes/oauth.d.ts +9 -0
- package/dist/presentation/routes/oauth.d.ts.map +1 -0
- package/dist/presentation/routes/oauth.js +150 -0
- package/dist/presentation/routes/oauth.js.map +1 -0
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +24 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/temporal/activities/agent-access-retention.d.ts +13 -0
- package/dist/temporal/activities/agent-access-retention.d.ts.map +1 -0
- package/dist/temporal/activities/agent-access-retention.js +58 -0
- package/dist/temporal/activities/agent-access-retention.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +5 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +8 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/constants.d.ts +2 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +3 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/agent-access-retention-cron.d.ts +2 -0
- package/dist/temporal/workflows/agent-access-retention-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/agent-access-retention-cron.js +22 -0
- package/dist/temporal/workflows/agent-access-retention-cron.js.map +1 -0
- package/dist/temporal/workflows/index.bundle.js +25227 -0
- package/dist/temporal/workflows/index.bundle.meta.json +1 -0
- package/dist/temporal/workflows/index.d.ts +2 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +3 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -4
- package/src/core/cimd.test.ts +213 -0
- package/src/core/cimd.ts +532 -0
- package/src/core/errors.ts +41 -0
- package/src/core/oauth-client-resolver.test.ts +198 -0
- package/src/core/oauth-client-resolver.ts +253 -0
- package/src/core/oauth-client.test.ts +157 -0
- package/src/core/oauth-client.ts +331 -0
- package/src/db/agent-access-retention.ts +10 -0
- package/src/db/agent-access.test.ts +697 -3
- package/src/db/agent-access.ts +953 -91
- package/src/index.test.ts +27 -0
- package/src/index.ts +76 -0
- package/src/metrics/instance.ts +5 -1
- package/src/presentation/routes/oauth.test.ts +133 -0
- package/src/presentation/routes/oauth.ts +144 -0
- package/src/presentation/routes/rate-limit.ts +12 -0
- package/src/temporal/activities/agent-access-retention.ts +76 -0
- package/src/temporal/activities/index.ts +5 -0
- package/src/temporal/constants.ts +1 -0
- package/src/temporal/workflows/agent-access-retention-cron.ts +23 -0
- package/src/temporal/workflows/index.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { oauthAuthorizationServerMetadataSchema, oauthDynamicClientRegistrationRequestSchema, oauthDynamicClientRegistrationResponseSchema, oauthProtectedResourceMetadataSchema } from '@shipfox/api-auth-dto';
|
|
2
|
+
import { ClientError, defineRoute } from '@shipfox/node-fastify';
|
|
3
|
+
import { InvalidOAuthClientMetadataError, InvalidOAuthConfigurationError, OAuthMetadataFetchError, OAuthRedirectUriNotRegisteredError } from '#core/errors.js';
|
|
4
|
+
import { validateOAuthPublicOrigin } from '#core/oauth-client.js';
|
|
5
|
+
import { registerOAuthClient } from '#core/oauth-client-resolver.js';
|
|
6
|
+
import { createAuthIpRateLimitPreHandler } from './rate-limit.js';
|
|
7
|
+
function apiPublicOrigin(options) {
|
|
8
|
+
return validateOAuthPublicOrigin(options.apiPublicUrl);
|
|
9
|
+
}
|
|
10
|
+
function translateOAuthError(error) {
|
|
11
|
+
if (error instanceof InvalidOAuthClientMetadataError || error instanceof OAuthMetadataFetchError || error instanceof OAuthRedirectUriNotRegisteredError) {
|
|
12
|
+
throw new ClientError('OAuth client metadata is invalid', 'invalid-client-metadata', {
|
|
13
|
+
status: 400
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (error instanceof InvalidOAuthConfigurationError) {
|
|
17
|
+
throw new ClientError('OAuth configuration is invalid', 'oauth-configuration-invalid', {
|
|
18
|
+
status: 500
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
function registrationResponse(result) {
|
|
24
|
+
return {
|
|
25
|
+
client_id: result.metadata.clientId,
|
|
26
|
+
client_name: result.metadata.clientName,
|
|
27
|
+
redirect_uris: result.metadata.redirectUris,
|
|
28
|
+
grant_types: result.metadata.grantTypes,
|
|
29
|
+
response_types: result.metadata.responseTypes,
|
|
30
|
+
token_endpoint_auth_method: result.metadata.tokenEndpointAuthMethod,
|
|
31
|
+
scope: result.metadata.scope
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function createProtectedResourceMetadataRoute(origin) {
|
|
35
|
+
return defineRoute({
|
|
36
|
+
method: 'GET',
|
|
37
|
+
path: '/.well-known/oauth-protected-resource',
|
|
38
|
+
description: 'Describe the protected MCP resource and its authorization server.',
|
|
39
|
+
schema: {
|
|
40
|
+
response: {
|
|
41
|
+
200: oauthProtectedResourceMetadataSchema
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
handler: ()=>({
|
|
45
|
+
resource: `${origin}/mcp`,
|
|
46
|
+
authorization_servers: [
|
|
47
|
+
origin
|
|
48
|
+
],
|
|
49
|
+
scopes_supported: [
|
|
50
|
+
'read'
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function createAuthorizationServerMetadataRoute(origin) {
|
|
56
|
+
return defineRoute({
|
|
57
|
+
method: 'GET',
|
|
58
|
+
path: '/.well-known/oauth-authorization-server',
|
|
59
|
+
description: 'Describe the MCP OAuth authorization server.',
|
|
60
|
+
schema: {
|
|
61
|
+
response: {
|
|
62
|
+
200: oauthAuthorizationServerMetadataSchema
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
handler: ()=>({
|
|
66
|
+
issuer: origin,
|
|
67
|
+
authorization_endpoint: `${origin}/oauth/authorize`,
|
|
68
|
+
token_endpoint: `${origin}/oauth/token`,
|
|
69
|
+
registration_endpoint: `${origin}/oauth/register`,
|
|
70
|
+
response_types_supported: [
|
|
71
|
+
'code'
|
|
72
|
+
],
|
|
73
|
+
grant_types_supported: [
|
|
74
|
+
'authorization_code',
|
|
75
|
+
'refresh_token'
|
|
76
|
+
],
|
|
77
|
+
code_challenge_methods_supported: [
|
|
78
|
+
'S256'
|
|
79
|
+
],
|
|
80
|
+
token_endpoint_auth_methods_supported: [
|
|
81
|
+
'none'
|
|
82
|
+
],
|
|
83
|
+
scopes_supported: [
|
|
84
|
+
'read'
|
|
85
|
+
],
|
|
86
|
+
client_id_metadata_document_supported: true
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function createDynamicRegistrationRoute() {
|
|
91
|
+
return defineRoute({
|
|
92
|
+
method: 'POST',
|
|
93
|
+
path: '/oauth/register',
|
|
94
|
+
description: 'Register a public OAuth client for MCP access.',
|
|
95
|
+
options: {
|
|
96
|
+
bodyLimit: 32 * 1024
|
|
97
|
+
},
|
|
98
|
+
schema: {
|
|
99
|
+
body: oauthDynamicClientRegistrationRequestSchema,
|
|
100
|
+
response: {
|
|
101
|
+
201: oauthDynamicClientRegistrationResponseSchema
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
preHandler: createAuthIpRateLimitPreHandler('oauth-register'),
|
|
105
|
+
errorHandler: translateOAuthError,
|
|
106
|
+
handler: async (request, reply)=>{
|
|
107
|
+
const result = await registerOAuthClient({
|
|
108
|
+
clientName: request.body.client_name,
|
|
109
|
+
redirectUris: request.body.redirect_uris,
|
|
110
|
+
grantTypes: request.body.grant_types,
|
|
111
|
+
responseTypes: request.body.response_types,
|
|
112
|
+
tokenEndpointAuthMethod: request.body.token_endpoint_auth_method,
|
|
113
|
+
scope: request.body.scope
|
|
114
|
+
});
|
|
115
|
+
reply.code(201);
|
|
116
|
+
return registrationResponse(result);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function createOAuthMetadataRoutes(options) {
|
|
121
|
+
const origin = apiPublicOrigin(options);
|
|
122
|
+
return {
|
|
123
|
+
prefix: '',
|
|
124
|
+
routes: [
|
|
125
|
+
createProtectedResourceMetadataRoute(origin),
|
|
126
|
+
createAuthorizationServerMetadataRoute(origin)
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function createOAuthClientIdentificationRoutes() {
|
|
131
|
+
return {
|
|
132
|
+
prefix: '',
|
|
133
|
+
routes: [
|
|
134
|
+
createDynamicRegistrationRoute()
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function createOAuthRoutes(options) {
|
|
139
|
+
const metadataRoutes = createOAuthMetadataRoutes(options);
|
|
140
|
+
const clientIdentificationRoutes = createOAuthClientIdentificationRoutes();
|
|
141
|
+
return {
|
|
142
|
+
prefix: '',
|
|
143
|
+
routes: [
|
|
144
|
+
...metadataRoutes.routes,
|
|
145
|
+
...clientIdentificationRoutes.routes
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/oauth.ts"],"sourcesContent":["import {\n type OAuthDynamicClientRegistrationResponseDto,\n oauthAuthorizationServerMetadataSchema,\n oauthDynamicClientRegistrationRequestSchema,\n oauthDynamicClientRegistrationResponseSchema,\n oauthProtectedResourceMetadataSchema,\n} from '@shipfox/api-auth-dto';\nimport {ClientError, defineRoute, type RouteGroup} from '@shipfox/node-fastify';\nimport {\n InvalidOAuthClientMetadataError,\n InvalidOAuthConfigurationError,\n OAuthMetadataFetchError,\n OAuthRedirectUriNotRegisteredError,\n} from '#core/errors.js';\nimport {validateOAuthPublicOrigin} from '#core/oauth-client.js';\nimport {registerOAuthClient} from '#core/oauth-client-resolver.js';\nimport {createAuthIpRateLimitPreHandler} from './rate-limit.js';\n\nexport interface CreateOAuthRoutesOptions {\n /** Validated here and deliberately injected until production composition. */\n apiPublicUrl: string;\n}\n\nfunction apiPublicOrigin(options: CreateOAuthRoutesOptions): string {\n return validateOAuthPublicOrigin(options.apiPublicUrl);\n}\n\nfunction translateOAuthError(error: unknown): never {\n if (\n error instanceof InvalidOAuthClientMetadataError ||\n error instanceof OAuthMetadataFetchError ||\n error instanceof OAuthRedirectUriNotRegisteredError\n ) {\n throw new ClientError('OAuth client metadata is invalid', 'invalid-client-metadata', {\n status: 400,\n });\n }\n if (error instanceof InvalidOAuthConfigurationError) {\n throw new ClientError('OAuth configuration is invalid', 'oauth-configuration-invalid', {\n status: 500,\n });\n }\n throw error;\n}\n\nfunction registrationResponse(\n result: Awaited<ReturnType<typeof registerOAuthClient>>,\n): OAuthDynamicClientRegistrationResponseDto {\n return {\n client_id: result.metadata.clientId,\n client_name: result.metadata.clientName,\n redirect_uris: result.metadata.redirectUris,\n grant_types: result.metadata.grantTypes,\n response_types: result.metadata.responseTypes,\n token_endpoint_auth_method: result.metadata.tokenEndpointAuthMethod,\n scope: result.metadata.scope,\n };\n}\n\nfunction createProtectedResourceMetadataRoute(origin: string) {\n return defineRoute({\n method: 'GET',\n path: '/.well-known/oauth-protected-resource',\n description: 'Describe the protected MCP resource and its authorization server.',\n schema: {response: {200: oauthProtectedResourceMetadataSchema}},\n handler: () => ({\n resource: `${origin}/mcp`,\n authorization_servers: [origin],\n scopes_supported: ['read'],\n }),\n });\n}\n\nfunction createAuthorizationServerMetadataRoute(origin: string) {\n return defineRoute({\n method: 'GET',\n path: '/.well-known/oauth-authorization-server',\n description: 'Describe the MCP OAuth authorization server.',\n schema: {response: {200: oauthAuthorizationServerMetadataSchema}},\n handler: () => ({\n issuer: origin,\n authorization_endpoint: `${origin}/oauth/authorize`,\n token_endpoint: `${origin}/oauth/token`,\n registration_endpoint: `${origin}/oauth/register`,\n response_types_supported: ['code'],\n grant_types_supported: ['authorization_code', 'refresh_token'],\n code_challenge_methods_supported: ['S256'],\n token_endpoint_auth_methods_supported: ['none'],\n scopes_supported: ['read'],\n client_id_metadata_document_supported: true,\n }),\n });\n}\n\nfunction createDynamicRegistrationRoute() {\n return defineRoute({\n method: 'POST',\n path: '/oauth/register',\n description: 'Register a public OAuth client for MCP access.',\n options: {bodyLimit: 32 * 1024},\n schema: {\n body: oauthDynamicClientRegistrationRequestSchema,\n response: {201: oauthDynamicClientRegistrationResponseSchema},\n },\n preHandler: createAuthIpRateLimitPreHandler('oauth-register'),\n errorHandler: translateOAuthError,\n handler: async (request, reply) => {\n const result = await registerOAuthClient({\n clientName: request.body.client_name,\n redirectUris: request.body.redirect_uris,\n grantTypes: request.body.grant_types,\n responseTypes: request.body.response_types,\n tokenEndpointAuthMethod: request.body.token_endpoint_auth_method,\n scope: request.body.scope,\n });\n reply.code(201);\n return registrationResponse(result);\n },\n });\n}\n\nexport function createOAuthMetadataRoutes(options: CreateOAuthRoutesOptions): RouteGroup {\n const origin = apiPublicOrigin(options);\n return {\n prefix: '',\n routes: [\n createProtectedResourceMetadataRoute(origin),\n createAuthorizationServerMetadataRoute(origin),\n ],\n };\n}\n\nexport function createOAuthClientIdentificationRoutes(): RouteGroup {\n return {prefix: '', routes: [createDynamicRegistrationRoute()]};\n}\n\nexport function createOAuthRoutes(options: CreateOAuthRoutesOptions): RouteGroup {\n const metadataRoutes = createOAuthMetadataRoutes(options);\n const clientIdentificationRoutes = createOAuthClientIdentificationRoutes();\n return {\n prefix: '',\n routes: [...metadataRoutes.routes, ...clientIdentificationRoutes.routes],\n };\n}\n"],"names":["oauthAuthorizationServerMetadataSchema","oauthDynamicClientRegistrationRequestSchema","oauthDynamicClientRegistrationResponseSchema","oauthProtectedResourceMetadataSchema","ClientError","defineRoute","InvalidOAuthClientMetadataError","InvalidOAuthConfigurationError","OAuthMetadataFetchError","OAuthRedirectUriNotRegisteredError","validateOAuthPublicOrigin","registerOAuthClient","createAuthIpRateLimitPreHandler","apiPublicOrigin","options","apiPublicUrl","translateOAuthError","error","status","registrationResponse","result","client_id","metadata","clientId","client_name","clientName","redirect_uris","redirectUris","grant_types","grantTypes","response_types","responseTypes","token_endpoint_auth_method","tokenEndpointAuthMethod","scope","createProtectedResourceMetadataRoute","origin","method","path","description","schema","response","handler","resource","authorization_servers","scopes_supported","createAuthorizationServerMetadataRoute","issuer","authorization_endpoint","token_endpoint","registration_endpoint","response_types_supported","grant_types_supported","code_challenge_methods_supported","token_endpoint_auth_methods_supported","client_id_metadata_document_supported","createDynamicRegistrationRoute","bodyLimit","body","preHandler","errorHandler","request","reply","code","createOAuthMetadataRoutes","prefix","routes","createOAuthClientIdentificationRoutes","createOAuthRoutes","metadataRoutes","clientIdentificationRoutes"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../src/presentation/routes/rate-limit.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,KAAK,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,qBAAqB,CAAC;AAmD7B,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAgDD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,aAChD,cAAc,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAiB9F;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,aAClD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAS3E;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,mBAAmB,aACrD,cAAc,SAAS,YAAY,KAAG,OAAO,CAAC,IAAI,CAAC,CAmB3E"}
|
|
@@ -66,6 +66,30 @@ const policies = {
|
|
|
66
66
|
limit: 20,
|
|
67
67
|
windowSeconds: 15 * 60
|
|
68
68
|
}
|
|
69
|
+
},
|
|
70
|
+
'oauth-register': {
|
|
71
|
+
ip: {
|
|
72
|
+
limit: 10,
|
|
73
|
+
windowSeconds: 60 * 60
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
'oauth-cimd': {
|
|
77
|
+
ip: {
|
|
78
|
+
limit: 30,
|
|
79
|
+
windowSeconds: 60 * 60
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
'oauth-authorize': {
|
|
83
|
+
ip: {
|
|
84
|
+
limit: 30,
|
|
85
|
+
windowSeconds: 5 * 60
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
'oauth-token': {
|
|
89
|
+
ip: {
|
|
90
|
+
limit: 60,
|
|
91
|
+
windowSeconds: 5 * 60
|
|
92
|
+
}
|
|
69
93
|
}
|
|
70
94
|
};
|
|
71
95
|
function routeName(request) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {getUserContext} from '@shipfox/api-auth-context';\nimport {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n directory: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n actor: {limit: 60, windowSeconds: 5 * 60},\n },\n impersonate: {\n // Bounds mint and probing attempts: the IP bucket bounds aggregate source\n // traffic, and the actor bucket is a per-actor cap that prevents an actor\n // from evading the bound by rotating IPs (every request still consumes the\n // IP bucket first, so a shared NAT cannot exhaust it faster by pooling\n // actors). Ladder denials by an identifiable actor role are audited as\n // `failed` events.\n ip: {limit: 20, windowSeconds: 15 * 60},\n actor: {limit: 20, windowSeconds: 15 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n\n/**\n * Rate limits an authenticated route by source IP and by actor. Every request\n * consumes the IP bucket, which bounds aggregate source traffic; the actor\n * bucket is an additional per-actor cap that prevents an actor from evading\n * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`\n * hook), so the user context is set.\n */\nexport function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n const client = getUserContext(request);\n if (client) {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'actor',\n identifier: client.userId,\n });\n }\n };\n}\n"],"names":["getUserContext","ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","directory","actor","impersonate","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler","createAuthActorRateLimitPreHandler","client","userId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/presentation/routes/rate-limit.ts"],"sourcesContent":["import {getUserContext} from '@shipfox/api-auth-context';\nimport {ClientError, type FastifyReply, type FastifyRequest} from '@shipfox/node-fastify';\nimport {enforceRateLimit as enforceSharedRateLimit} from '@shipfox/node-rate-limit';\nimport {\n type AuthRateLimitAction,\n type AuthRateLimitPolicy,\n type AuthRateLimitScope,\n checkAuthRateLimit,\n} from '#core/rate-limit.js';\n\nconst policies: Record<\n AuthRateLimitAction,\n Partial<Record<AuthRateLimitScope, AuthRateLimitPolicy>>\n> = {\n login: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n email: {limit: 10, windowSeconds: 15 * 60},\n },\n 'email-send': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n email: {limit: 3, windowSeconds: 60 * 60},\n },\n bootstrap: {\n ip: {limit: 5, windowSeconds: 15 * 60},\n },\n 'bootstrap-state': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n lookup: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n directory: {\n ip: {limit: 60, windowSeconds: 5 * 60},\n actor: {limit: 60, windowSeconds: 5 * 60},\n },\n impersonate: {\n // Bounds mint and probing attempts: the IP bucket bounds aggregate source\n // traffic, and the actor bucket is a per-actor cap that prevents an actor\n // from evading the bound by rotating IPs (every request still consumes the\n // IP bucket first, so a shared NAT cannot exhaust it faster by pooling\n // actors). Ladder denials by an identifiable actor role are audited as\n // `failed` events.\n ip: {limit: 20, windowSeconds: 15 * 60},\n actor: {limit: 20, windowSeconds: 15 * 60},\n },\n 'oauth-register': {\n ip: {limit: 10, windowSeconds: 60 * 60},\n },\n 'oauth-cimd': {\n ip: {limit: 30, windowSeconds: 60 * 60},\n },\n 'oauth-authorize': {\n ip: {limit: 30, windowSeconds: 5 * 60},\n },\n 'oauth-token': {\n ip: {limit: 60, windowSeconds: 5 * 60},\n },\n};\n\ninterface EmailBody {\n email: string;\n}\n\nfunction routeName(request: FastifyRequest): string {\n return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';\n}\n\nasync function enforceRateLimit(params: {\n request: FastifyRequest;\n reply: FastifyReply;\n action: AuthRateLimitAction;\n scope: AuthRateLimitScope;\n identifier: string;\n}): Promise<void> {\n const policy = policies[params.action][params.scope];\n if (!policy) return;\n\n await enforceSharedRateLimit({\n request: params.request,\n reply: params.reply,\n check: () =>\n checkAuthRateLimit({\n action: params.action,\n scope: params.scope,\n identifier: params.identifier,\n ...policy,\n }),\n route: routeName(params.request),\n unavailableCode: 'auth-rate-limit-unavailable',\n unavailableMessage: 'Authentication rate limiter unavailable',\n setRetryAfter: (reply, retryAfterSeconds) => {\n reply.header('Retry-After', String(retryAfterSeconds));\n },\n logWarn: (request, context) => {\n request.log.warn(context, 'Auth rate limit blocked request');\n },\n logError: (request, context) => {\n request.log.error(context, 'Auth rate limiter unavailable');\n },\n createClientError: (presentation, cause) =>\n new ClientError(presentation.message, presentation.code, {\n status: presentation.status,\n ...(presentation.details ? {details: presentation.details} : {}),\n data: presentation.data,\n cause,\n }),\n });\n}\n\nexport function createAuthRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest<{Body: EmailBody}>, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'email',\n identifier: request.body.email,\n });\n };\n}\n\nexport function createAuthIpRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n };\n}\n\n/**\n * Rate limits an authenticated route by source IP and by actor. Every request\n * consumes the IP bucket, which bounds aggregate source traffic; the actor\n * bucket is an additional per-actor cap that prevents an actor from evading\n * the bound by rotating IPs. Runs after authentication (auth is an `onRequest`\n * hook), so the user context is set.\n */\nexport function createAuthActorRateLimitPreHandler(action: AuthRateLimitAction) {\n return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'ip',\n identifier: request.ip,\n });\n const client = getUserContext(request);\n if (client) {\n await enforceRateLimit({\n request,\n reply,\n action,\n scope: 'actor',\n identifier: client.userId,\n });\n }\n };\n}\n"],"names":["getUserContext","ClientError","enforceRateLimit","enforceSharedRateLimit","checkAuthRateLimit","policies","login","ip","limit","windowSeconds","email","bootstrap","lookup","directory","actor","impersonate","routeName","request","routeOptions","url","split","params","policy","action","scope","reply","check","identifier","route","unavailableCode","unavailableMessage","setRetryAfter","retryAfterSeconds","header","String","logWarn","context","log","warn","logError","error","createClientError","presentation","cause","message","code","status","details","data","createAuthRateLimitPreHandler","body","createAuthIpRateLimitPreHandler","createAuthActorRateLimitPreHandler","client","userId"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AgentAccessRetentionResult {
|
|
2
|
+
deleted: number;
|
|
3
|
+
transitioned: number;
|
|
4
|
+
iterations: number;
|
|
5
|
+
timedOut: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Drains bounded retention batches for the current tick. Heartbeats continue
|
|
9
|
+
* while a database batch waits on a lock, and the wall-clock budget keeps the
|
|
10
|
+
* activity inside its five-minute Temporal start-to-close timeout.
|
|
11
|
+
*/
|
|
12
|
+
export declare function agentAccessRetentionActivity(): Promise<AgentAccessRetentionResult>;
|
|
13
|
+
//# sourceMappingURL=agent-access-retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-access-retention.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/agent-access-retention.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAWD;;;;GAIG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,0BAA0B,CAAC,CA8CxF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Context } from '@temporalio/activity';
|
|
2
|
+
import { pruneAgentAccessBatch } from '#db/agent-access.js';
|
|
3
|
+
import { AGENT_ACCESS_RETENTION_BATCH_LIMIT, AGENT_ACCESS_RETENTION_MAX_ITERATIONS, AGENT_ACCESS_RETENTION_TIME_BUDGET_MS } from '#db/agent-access-retention.js';
|
|
4
|
+
function isAgentAccessRetentionTimeout(error) {
|
|
5
|
+
if (typeof error !== 'object' || error === null) return false;
|
|
6
|
+
const details = error;
|
|
7
|
+
if (details.code === '55P03' || details.code === '57014') return true;
|
|
8
|
+
return details.cause !== undefined && details.cause !== error ? isAgentAccessRetentionTimeout(details.cause) : false;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Drains bounded retention batches for the current tick. Heartbeats continue
|
|
12
|
+
* while a database batch waits on a lock, and the wall-clock budget keeps the
|
|
13
|
+
* activity inside its five-minute Temporal start-to-close timeout.
|
|
14
|
+
*/ export async function agentAccessRetentionActivity() {
|
|
15
|
+
const ctx = Context.current();
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
const deadline = startedAt + AGENT_ACCESS_RETENTION_TIME_BUDGET_MS;
|
|
18
|
+
const result = {
|
|
19
|
+
deleted: 0,
|
|
20
|
+
transitioned: 0,
|
|
21
|
+
iterations: 0,
|
|
22
|
+
timedOut: false
|
|
23
|
+
};
|
|
24
|
+
const heartbeat = ()=>ctx.heartbeat(result);
|
|
25
|
+
const heartbeatTimer = setInterval(heartbeat, 15_000);
|
|
26
|
+
try {
|
|
27
|
+
while(result.iterations < AGENT_ACCESS_RETENTION_MAX_ITERATIONS){
|
|
28
|
+
if (Date.now() >= deadline) {
|
|
29
|
+
result.timedOut = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const batch = await pruneAgentAccessBatch({
|
|
34
|
+
limit: AGENT_ACCESS_RETENTION_BATCH_LIMIT,
|
|
35
|
+
deadlineMs: deadline
|
|
36
|
+
});
|
|
37
|
+
result.deleted += batch.deleted;
|
|
38
|
+
result.transitioned += batch.transitioned;
|
|
39
|
+
result.iterations += 1;
|
|
40
|
+
heartbeat();
|
|
41
|
+
if (batch.deleted === 0 && batch.transitioned === 0) break;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!isAgentAccessRetentionTimeout(error)) throw error;
|
|
44
|
+
result.timedOut = true;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (result.iterations >= AGENT_ACCESS_RETENTION_MAX_ITERATIONS) {
|
|
49
|
+
result.timedOut = true;
|
|
50
|
+
}
|
|
51
|
+
heartbeat();
|
|
52
|
+
return result;
|
|
53
|
+
} finally{
|
|
54
|
+
clearInterval(heartbeatTimer);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=agent-access-retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/activities/agent-access-retention.ts"],"sourcesContent":["import {Context} from '@temporalio/activity';\nimport {pruneAgentAccessBatch} from '#db/agent-access.js';\nimport {\n AGENT_ACCESS_RETENTION_BATCH_LIMIT,\n AGENT_ACCESS_RETENTION_MAX_ITERATIONS,\n AGENT_ACCESS_RETENTION_TIME_BUDGET_MS,\n} from '#db/agent-access-retention.js';\n\nexport interface AgentAccessRetentionResult {\n deleted: number;\n transitioned: number;\n iterations: number;\n timedOut: boolean;\n}\n\nfunction isAgentAccessRetentionTimeout(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false;\n const details = error as {cause?: unknown; code?: unknown};\n if (details.code === '55P03' || details.code === '57014') return true;\n return details.cause !== undefined && details.cause !== error\n ? isAgentAccessRetentionTimeout(details.cause)\n : false;\n}\n\n/**\n * Drains bounded retention batches for the current tick. Heartbeats continue\n * while a database batch waits on a lock, and the wall-clock budget keeps the\n * activity inside its five-minute Temporal start-to-close timeout.\n */\nexport async function agentAccessRetentionActivity(): Promise<AgentAccessRetentionResult> {\n const ctx = Context.current();\n const startedAt = Date.now();\n const deadline = startedAt + AGENT_ACCESS_RETENTION_TIME_BUDGET_MS;\n const result: AgentAccessRetentionResult = {\n deleted: 0,\n transitioned: 0,\n iterations: 0,\n timedOut: false,\n };\n const heartbeat = () => ctx.heartbeat(result);\n const heartbeatTimer = setInterval(heartbeat, 15_000);\n\n try {\n while (result.iterations < AGENT_ACCESS_RETENTION_MAX_ITERATIONS) {\n if (Date.now() >= deadline) {\n result.timedOut = true;\n break;\n }\n\n try {\n const batch = await pruneAgentAccessBatch({\n limit: AGENT_ACCESS_RETENTION_BATCH_LIMIT,\n deadlineMs: deadline,\n });\n result.deleted += batch.deleted;\n result.transitioned += batch.transitioned;\n result.iterations += 1;\n heartbeat();\n\n if (batch.deleted === 0 && batch.transitioned === 0) break;\n } catch (error) {\n if (!isAgentAccessRetentionTimeout(error)) throw error;\n result.timedOut = true;\n break;\n }\n }\n\n if (result.iterations >= AGENT_ACCESS_RETENTION_MAX_ITERATIONS) {\n result.timedOut = true;\n }\n heartbeat();\n return result;\n } finally {\n clearInterval(heartbeatTimer);\n }\n}\n"],"names":["Context","pruneAgentAccessBatch","AGENT_ACCESS_RETENTION_BATCH_LIMIT","AGENT_ACCESS_RETENTION_MAX_ITERATIONS","AGENT_ACCESS_RETENTION_TIME_BUDGET_MS","isAgentAccessRetentionTimeout","error","details","code","cause","undefined","agentAccessRetentionActivity","ctx","current","startedAt","Date","now","deadline","result","deleted","transitioned","iterations","timedOut","heartbeat","heartbeatTimer","setInterval","batch","limit","deadlineMs","clearInterval"],"mappings":"AAAA,SAAQA,OAAO,QAAO,uBAAuB;AAC7C,SAAQC,qBAAqB,QAAO,sBAAsB;AAC1D,SACEC,kCAAkC,EAClCC,qCAAqC,EACrCC,qCAAqC,QAChC,gCAAgC;AASvC,SAASC,8BAA8BC,KAAc;IACnD,IAAI,OAAOA,UAAU,YAAYA,UAAU,MAAM,OAAO;IACxD,MAAMC,UAAUD;IAChB,IAAIC,QAAQC,IAAI,KAAK,WAAWD,QAAQC,IAAI,KAAK,SAAS,OAAO;IACjE,OAAOD,QAAQE,KAAK,KAAKC,aAAaH,QAAQE,KAAK,KAAKH,QACpDD,8BAA8BE,QAAQE,KAAK,IAC3C;AACN;AAEA;;;;CAIC,GACD,OAAO,eAAeE;IACpB,MAAMC,MAAMZ,QAAQa,OAAO;IAC3B,MAAMC,YAAYC,KAAKC,GAAG;IAC1B,MAAMC,WAAWH,YAAYV;IAC7B,MAAMc,SAAqC;QACzCC,SAAS;QACTC,cAAc;QACdC,YAAY;QACZC,UAAU;IACZ;IACA,MAAMC,YAAY,IAAMX,IAAIW,SAAS,CAACL;IACtC,MAAMM,iBAAiBC,YAAYF,WAAW;IAE9C,IAAI;QACF,MAAOL,OAAOG,UAAU,GAAGlB,sCAAuC;YAChE,IAAIY,KAAKC,GAAG,MAAMC,UAAU;gBAC1BC,OAAOI,QAAQ,GAAG;gBAClB;YACF;YAEA,IAAI;gBACF,MAAMI,QAAQ,MAAMzB,sBAAsB;oBACxC0B,OAAOzB;oBACP0B,YAAYX;gBACd;gBACAC,OAAOC,OAAO,IAAIO,MAAMP,OAAO;gBAC/BD,OAAOE,YAAY,IAAIM,MAAMN,YAAY;gBACzCF,OAAOG,UAAU,IAAI;gBACrBE;gBAEA,IAAIG,MAAMP,OAAO,KAAK,KAAKO,MAAMN,YAAY,KAAK,GAAG;YACvD,EAAE,OAAOd,OAAO;gBACd,IAAI,CAACD,8BAA8BC,QAAQ,MAAMA;gBACjDY,OAAOI,QAAQ,GAAG;gBAClB;YACF;QACF;QAEA,IAAIJ,OAAOG,UAAU,IAAIlB,uCAAuC;YAC9De,OAAOI,QAAQ,GAAG;QACpB;QACAC;QACA,OAAOL;IACT,SAAU;QACRW,cAAcL;IAChB;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,4BAA4B,EAAC,MAAM,6BAA6B,CAAC;AAEzE,wBAAgB,+BAA+B;;EAE9C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/activities/index.ts"],"sourcesContent":["import {agentAccessRetentionActivity} from './agent-access-retention.js';\n\nexport function createAuthMaintenanceActivities() {\n return {agentAccessRetentionActivity};\n}\n"],"names":["agentAccessRetentionActivity","createAuthMaintenanceActivities"],"mappings":"AAAA,SAAQA,4BAA4B,QAAO,8BAA8B;AAEzE,OAAO,SAASC;IACd,OAAO;QAACD;IAA4B;AACtC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/temporal/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wCAAwC,kCAAkC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/temporal/constants.ts"],"sourcesContent":["export const AUTH_AGENT_ACCESS_MAINTENANCE_TASK_QUEUE = 'auth-agent-access-maintenance';\n"],"names":["AUTH_AGENT_ACCESS_MAINTENANCE_TASK_QUEUE"],"mappings":"AAAA,OAAO,MAAMA,2CAA2C,gCAAgC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-access-retention-cron.d.ts","sourceRoot":"","sources":["../../../src/temporal/workflows/agent-access-retention-cron.ts"],"names":[],"mappings":"AAYA,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC,CAU9D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { log, proxyActivities } from '@temporalio/workflow';
|
|
2
|
+
const { agentAccessRetentionActivity } = proxyActivities({
|
|
3
|
+
startToCloseTimeout: '5 minutes',
|
|
4
|
+
heartbeatTimeout: '1 minute',
|
|
5
|
+
// Retry on the next cron tick so a timed-out attempt cannot overlap its own sweep.
|
|
6
|
+
retry: {
|
|
7
|
+
maximumAttempts: 1
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
export async function agentAccessRetentionCron() {
|
|
11
|
+
const result = await agentAccessRetentionActivity();
|
|
12
|
+
if (result.deleted > 0 || result.transitioned > 0 || result.timedOut) {
|
|
13
|
+
log.info('Agent-access retention sweep complete', {
|
|
14
|
+
deleted: result.deleted,
|
|
15
|
+
transitioned: result.transitioned,
|
|
16
|
+
iterations: result.iterations,
|
|
17
|
+
timedOut: result.timedOut
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=agent-access-retention-cron.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/workflows/agent-access-retention-cron.ts"],"sourcesContent":["import {log, proxyActivities} from '@temporalio/workflow';\nimport type {createAuthMaintenanceActivities} from '../activities/index.js';\n\nconst {agentAccessRetentionActivity} = proxyActivities<\n ReturnType<typeof createAuthMaintenanceActivities>\n>({\n startToCloseTimeout: '5 minutes',\n heartbeatTimeout: '1 minute',\n // Retry on the next cron tick so a timed-out attempt cannot overlap its own sweep.\n retry: {maximumAttempts: 1},\n});\n\nexport async function agentAccessRetentionCron(): Promise<void> {\n const result = await agentAccessRetentionActivity();\n if (result.deleted > 0 || result.transitioned > 0 || result.timedOut) {\n log.info('Agent-access retention sweep complete', {\n deleted: result.deleted,\n transitioned: result.transitioned,\n iterations: result.iterations,\n timedOut: result.timedOut,\n });\n }\n}\n"],"names":["log","proxyActivities","agentAccessRetentionActivity","startToCloseTimeout","heartbeatTimeout","retry","maximumAttempts","agentAccessRetentionCron","result","deleted","transitioned","timedOut","info","iterations"],"mappings":"AAAA,SAAQA,GAAG,EAAEC,eAAe,QAAO,uBAAuB;AAG1D,MAAM,EAACC,4BAA4B,EAAC,GAAGD,gBAErC;IACAE,qBAAqB;IACrBC,kBAAkB;IAClB,mFAAmF;IACnFC,OAAO;QAACC,iBAAiB;IAAC;AAC5B;AAEA,OAAO,eAAeC;IACpB,MAAMC,SAAS,MAAMN;IACrB,IAAIM,OAAOC,OAAO,GAAG,KAAKD,OAAOE,YAAY,GAAG,KAAKF,OAAOG,QAAQ,EAAE;QACpEX,IAAIY,IAAI,CAAC,yCAAyC;YAChDH,SAASD,OAAOC,OAAO;YACvBC,cAAcF,OAAOE,YAAY;YACjCG,YAAYL,OAAOK,UAAU;YAC7BF,UAAUH,OAAOG,QAAQ;QAC3B;IACF;AACF"}
|