@intelicity/gates-sdk 0.1.6 → 0.2.1

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.
@@ -1,12 +1,11 @@
1
1
  import { jwtVerify, errors as joseErrors } from "jose";
2
2
  import { getJwks } from "../cache/jwks-cache.js";
3
- import { InvalidParameterError, MissingParameterError, TokenExpiredError, InvalidTokenError, UnauthorizedGroupError, } from "../errors/error.js";
4
- export class GatesAuthService {
3
+ import { InvalidParameterError, MissingParameterError, TokenExpiredError, InvalidTokenError, } from "../errors/error.js";
4
+ export class AuthService {
5
5
  region;
6
6
  userPoolId;
7
- audience;
8
- requiredGroup;
9
- constructor(region, userPoolId, audience, requiredGroup) {
7
+ clientIds;
8
+ constructor(region, userPoolId, clientId) {
10
9
  if (!region || typeof region !== "string" || region.trim().length === 0) {
11
10
  throw new MissingParameterError("region");
12
11
  }
@@ -15,35 +14,18 @@ export class GatesAuthService {
15
14
  userPoolId.trim().length === 0) {
16
15
  throw new MissingParameterError("userPoolId");
17
16
  }
18
- if (!audience ||
19
- typeof audience !== "string" ||
20
- audience.trim().length === 0) {
21
- throw new MissingParameterError("audience");
22
- }
23
- // Validar formato do userPoolId (deve seguir padrão AWS)
24
17
  if (!/^[a-zA-Z0-9_-]+$/.test(userPoolId)) {
25
- throw new InvalidParameterError("userPoolId", "deve seguir o formato AWS (apenas alfanuméricos, hífens e underscores)");
18
+ throw new InvalidParameterError("userPoolId", "must follow AWS format (alphanumeric, hyphens, and underscores only)");
26
19
  }
27
20
  this.region = region;
28
21
  this.userPoolId = userPoolId;
29
- this.audience = audience;
30
- this.requiredGroup = requiredGroup;
22
+ this.clientIds = clientId
23
+ ? Array.isArray(clientId) ? clientId : [clientId]
24
+ : [];
31
25
  }
32
26
  get issuer() {
33
27
  return `https://cognito-idp.${this.region}.amazonaws.com/${this.userPoolId}`;
34
28
  }
35
- isMemberOf(groups = []) {
36
- if (!Array.isArray(groups)) {
37
- return false;
38
- }
39
- if (!this.requiredGroup) {
40
- return true;
41
- }
42
- const requiredGroups = Array.isArray(this.requiredGroup)
43
- ? this.requiredGroup
44
- : [this.requiredGroup];
45
- return groups.some((g) => requiredGroups.includes(g));
46
- }
47
29
  async verifyToken(token) {
48
30
  if (!token) {
49
31
  throw new MissingParameterError("token");
@@ -52,47 +34,50 @@ export class GatesAuthService {
52
34
  const jwks = getJwks(this.region, this.userPoolId);
53
35
  const { payload } = await jwtVerify(token, jwks, {
54
36
  issuer: this.issuer,
55
- audience: this.audience,
56
37
  });
57
- if (this.requiredGroup) {
58
- const userGroups = payload["cognito:groups"];
59
- const requiredGroups = Array.isArray(this.requiredGroup)
60
- ? this.requiredGroup
61
- : [this.requiredGroup];
62
- if (!userGroups || !Array.isArray(userGroups)) {
63
- throw new UnauthorizedGroupError("Usuário não pertence a nenhum grupo obrigatório", requiredGroups);
38
+ const tokenUse = payload.token_use;
39
+ if (tokenUse !== "access" && tokenUse !== "id") {
40
+ throw new InvalidTokenError(`Unsupported token_use: expected "access" or "id", got "${tokenUse}"`);
41
+ }
42
+ if (this.clientIds.length > 0) {
43
+ if (tokenUse === "access") {
44
+ const clientId = payload.client_id;
45
+ if (!clientId || !this.clientIds.includes(clientId)) {
46
+ throw new InvalidTokenError("Token client_id does not match any expected clientId");
47
+ }
64
48
  }
65
- // Verifica se o usuário tem pelo menos um dos grupos obrigatórios
66
- const hasRequiredGroup = requiredGroups.some((group) => userGroups.includes(group));
67
- if (!hasRequiredGroup) {
68
- throw new UnauthorizedGroupError(`Usuário deve ser membro de um dos seguintes grupos: ${requiredGroups.join(", ")}`, requiredGroups);
49
+ else {
50
+ const aud = payload.aud;
51
+ const audValue = Array.isArray(aud) ? aud[0] : aud;
52
+ if (!audValue || !this.clientIds.includes(audValue)) {
53
+ throw new InvalidTokenError("Token audience does not match any expected clientId");
54
+ }
69
55
  }
70
56
  }
71
- // Mapear o payload do Cognito para o formato do GatesUser
57
+ const groups = payload["cognito:groups"] ?? [];
72
58
  const user = {
73
59
  user_id: payload.sub,
74
60
  email: payload.email,
75
61
  name: payload.name,
76
62
  role: payload["custom:general_role"],
63
+ groups,
64
+ token_use: tokenUse,
77
65
  exp: payload.exp,
78
66
  iat: payload.iat,
79
67
  };
80
68
  return user;
81
69
  }
82
70
  catch (error) {
83
- // Re-throw known errors
84
- if (error instanceof UnauthorizedGroupError ||
71
+ if (error instanceof InvalidTokenError ||
85
72
  error instanceof MissingParameterError) {
86
73
  throw error;
87
74
  }
88
- // Handle jose-specific errors
89
75
  if (error instanceof joseErrors.JWTExpired) {
90
- throw new TokenExpiredError("Token expirado");
76
+ throw new TokenExpiredError();
91
77
  }
92
78
  if (error instanceof joseErrors.JWTInvalid) {
93
- throw new InvalidTokenError("Token inválido ou malformado");
79
+ throw new InvalidTokenError("Invalid or malformed token");
94
80
  }
95
- // Handle other jose errors
96
81
  if (error instanceof Error) {
97
82
  if (error.message.includes("expired")) {
98
83
  throw new TokenExpiredError(error.message);
@@ -101,9 +86,9 @@ export class GatesAuthService {
101
86
  error.message.includes("invalid")) {
102
87
  throw new InvalidTokenError(error.message);
103
88
  }
104
- throw new InvalidTokenError(`Falha na verificação do token: ${error.message}`);
89
+ throw new InvalidTokenError(`Token verification failed: ${error.message}`);
105
90
  }
106
- throw new InvalidTokenError("Falha na verificação do token");
91
+ throw new InvalidTokenError("Token verification failed");
107
92
  }
108
93
  }
109
94
  }
@@ -0,0 +1,22 @@
1
+ export type ClientCredentialsConfig = {
2
+ domain: string;
3
+ clientId: string;
4
+ clientSecret: string;
5
+ scopes?: string[];
6
+ };
7
+ export type ClientToken = {
8
+ access_token: string;
9
+ expires_in: number;
10
+ token_type: string;
11
+ };
12
+ export declare class GatesClientAuth {
13
+ private readonly tokenEndpoint;
14
+ private readonly clientId;
15
+ private readonly clientSecret;
16
+ private readonly scopes;
17
+ private cachedToken;
18
+ constructor(config: ClientCredentialsConfig);
19
+ getAccessToken(): Promise<string>;
20
+ clearCache(): void;
21
+ }
22
+ //# sourceMappingURL=client-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-auth.d.ts","sourceRoot":"","sources":["../../src/services/client-auth.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,WAAW,CAAqD;gBAE5D,MAAM,EAAE,uBAAuB;IAkBrC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAiEvC,UAAU,IAAI,IAAI;CAGnB"}
@@ -0,0 +1,75 @@
1
+ import { MissingParameterError, ClientCredentialsError, } from "../errors/error.js";
2
+ export class GatesClientAuth {
3
+ tokenEndpoint;
4
+ clientId;
5
+ clientSecret;
6
+ scopes;
7
+ cachedToken = null;
8
+ constructor(config) {
9
+ if (!config.domain || config.domain.trim().length === 0) {
10
+ throw new MissingParameterError("domain");
11
+ }
12
+ if (!config.clientId || config.clientId.trim().length === 0) {
13
+ throw new MissingParameterError("clientId");
14
+ }
15
+ if (!config.clientSecret || config.clientSecret.trim().length === 0) {
16
+ throw new MissingParameterError("clientSecret");
17
+ }
18
+ const domain = config.domain.replace(/\/$/, "");
19
+ this.tokenEndpoint = `https://${domain}/oauth2/token`;
20
+ this.clientId = config.clientId;
21
+ this.clientSecret = config.clientSecret;
22
+ this.scopes = config.scopes ?? [];
23
+ }
24
+ async getAccessToken() {
25
+ if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {
26
+ return this.cachedToken.token;
27
+ }
28
+ const body = new URLSearchParams({
29
+ grant_type: "client_credentials",
30
+ });
31
+ if (this.scopes.length > 0) {
32
+ body.set("scope", this.scopes.join(" "));
33
+ }
34
+ const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64");
35
+ let response;
36
+ try {
37
+ response = await fetch(this.tokenEndpoint, {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/x-www-form-urlencoded",
41
+ Authorization: `Basic ${credentials}`,
42
+ },
43
+ body: body.toString(),
44
+ });
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : "Unknown network error";
48
+ throw new ClientCredentialsError(`Failed to reach token endpoint: ${message}`);
49
+ }
50
+ if (!response.ok) {
51
+ let detail;
52
+ try {
53
+ const errorBody = (await response.json());
54
+ detail = errorBody.error ?? response.statusText;
55
+ }
56
+ catch {
57
+ detail = response.statusText;
58
+ }
59
+ throw new ClientCredentialsError(`Token request failed (${response.status}): ${detail}`);
60
+ }
61
+ const data = (await response.json());
62
+ if (!data.access_token) {
63
+ throw new ClientCredentialsError("Token response missing access_token field");
64
+ }
65
+ const bufferMs = 30_000;
66
+ this.cachedToken = {
67
+ token: data.access_token,
68
+ expiresAt: Date.now() + data.expires_in * 1000 - bufferMs,
69
+ };
70
+ return data.access_token;
71
+ }
72
+ clearCache() {
73
+ this.cachedToken = null;
74
+ }
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intelicity/gates-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "Simple SDK for authenticating users with AWS Cognito JWT tokens",
5
5
  "type": "module",
6
6
  "exports": "./dist/index.js",
@@ -18,6 +18,8 @@
18
18
  "scripts": {
19
19
  "build": "tsc && tsc-alias",
20
20
  "typecheck": "tsc --noEmit",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
21
23
  "test:token": "tsx scripts/test-token.ts",
22
24
  "prepublishOnly": "npm run build"
23
25
  },
@@ -37,7 +39,8 @@
37
39
  "@types/node": "^24.1.0",
38
40
  "tsc-alias": "^1.8.16",
39
41
  "tsx": "^4.20.3",
40
- "typescript": "^5.9.2"
42
+ "typescript": "^5.9.2",
43
+ "vitest": "^4.1.0"
41
44
  },
42
45
  "dependencies": {
43
46
  "jose": "^6.1.1"