@greatapps/common 1.1.736 → 1.1.738

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.
@@ -3,13 +3,14 @@ import { cookies, headers } from "next/headers";
3
3
  import { DEFAULT_LOCALE, LOCALE_COOKIE } from "./config";
4
4
  import { localeFromCountry } from "./country-language";
5
5
  import { normalizeLocale } from "./normalize";
6
+ import { decodeJwtPayload } from "../utils/jwt-payload";
6
7
  const AUTH_COOKIE = "greatapps";
7
8
  const GEO_HEADER = "cf-ipcountry";
8
9
  const DEFAULT_WHITELABEL_ID = 1;
9
10
  function decodeJwtLocaleInfo(token) {
10
11
  if (!token) return null;
11
12
  try {
12
- const payload = JSON.parse(atob(token.split(".")[1]));
13
+ const payload = decodeJwtPayload(token);
13
14
  return {
14
15
  id_wl: payload?.id_wl != null ? Number(payload.id_wl) : void 0,
15
16
  country: payload?.session?.location?.country ?? null
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/i18n/resolve-locale.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies, headers } from 'next/headers';\nimport { DEFAULT_LOCALE, Locale, LOCALE_COOKIE } from './config';\nimport { localeFromCountry } from './country-language';\nimport { normalizeLocale } from './normalize';\n\nconst AUTH_COOKIE = 'greatapps';\nconst GEO_HEADER = 'cf-ipcountry';\nconst DEFAULT_WHITELABEL_ID = 1;\n\ninterface JwtLocaleInfo {\n id_wl?: number;\n country?: string | null;\n}\n\n/** Lê id_wl e país do JWT sem custo de rede (best-effort). */\nfunction decodeJwtLocaleInfo(token?: string | null): JwtLocaleInfo | null {\n if (!token) return null;\n try {\n const payload = JSON.parse(atob(token.split('.')[1]));\n return {\n id_wl: payload?.id_wl != null ? Number(payload.id_wl) : undefined,\n country: payload?.session?.location?.country ?? null,\n };\n } catch {\n return null;\n }\n}\n\nexport interface ResolveLocaleOptions {\n /**\n * Whitelabel id já resolvido (ex.: via `findWhitelabel()`), com prioridade sobre o JWT.\n * Permite detectar domínio whitelabel mesmo para visitante não-logado.\n */\n whitelabelId?: number | null;\n /** Idioma da conta (`user.language`), quando já conhecido pelo chamador. */\n userLanguage?: string | null;\n}\n\n/**\n * Resolve o locale da requisição (server-side). Ordem de prioridade:\n *\n * 1. **Whitelabel não-padrão** (`id_wl != 1`) → `pt` (por enquanto, whitelabels não são localizadas).\n * 2. **Preferência da conta** (`userLanguage`), quando fornecida.\n * 3. **Cookie `NEXT_LOCALE`** — cache da preferência ou escolha manual do visitante.\n * 4. **Inferência por país** — JWT (`session.location.country`) ou header `CF-IPCountry`.\n * 5. **Fallback** → {@link DEFAULT_LOCALE}.\n */\nexport async function resolveLocale(options: ResolveLocaleOptions = {}): Promise<Locale> {\n const cookieStore = await cookies();\n const token = process.env.DUMMY_AUTH_TOKEN || cookieStore.get(AUTH_COOKIE)?.value;\n const jwt = decodeJwtLocaleInfo(token);\n\n // 1. Whitelabel não-padrão → português\n const whitelabelId = options.whitelabelId ?? jwt?.id_wl ?? null;\n if (whitelabelId != null && whitelabelId !== DEFAULT_WHITELABEL_ID) {\n return 'pt-br';\n }\n\n // 2. Preferência explícita da conta\n const fromUser = normalizeLocale(options.userLanguage);\n if (fromUser) return fromUser;\n\n // 3. Cookie persistido\n const fromCookie = normalizeLocale(cookieStore.get(LOCALE_COOKIE)?.value);\n if (fromCookie) return fromCookie;\n\n // 4. Inferência por país (geo)\n const headersList = await headers();\n const country = jwt?.country ?? headersList.get(GEO_HEADER);\n const fromGeo = localeFromCountry(country);\n if (fromGeo) return fromGeo;\n\n // 5. Fallback\n return DEFAULT_LOCALE;\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,SAAS,eAAe;AACjC,SAAS,gBAAwB,qBAAqB;AACtD,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAEhC,MAAM,cAAc;AACpB,MAAM,aAAa;AACnB,MAAM,wBAAwB;AAQ9B,SAAS,oBAAoB,OAA6C;AACxE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,KAAK,MAAM,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC;AACpD,WAAO;AAAA,MACL,OAAO,SAAS,SAAS,OAAO,OAAO,QAAQ,KAAK,IAAI;AAAA,MACxD,SAAS,SAAS,SAAS,UAAU,WAAW;AAAA,IAClD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAqBA,eAAsB,cAAc,UAAgC,CAAC,GAAoB;AACvF,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,QAAQ,IAAI,oBAAoB,YAAY,IAAI,WAAW,GAAG;AAC5E,QAAM,MAAM,oBAAoB,KAAK;AAGrC,QAAM,eAAe,QAAQ,gBAAgB,KAAK,SAAS;AAC3D,MAAI,gBAAgB,QAAQ,iBAAiB,uBAAuB;AAClE,WAAO;AAAA,EACT;AAGA,QAAM,WAAW,gBAAgB,QAAQ,YAAY;AACrD,MAAI,SAAU,QAAO;AAGrB,QAAM,aAAa,gBAAgB,YAAY,IAAI,aAAa,GAAG,KAAK;AACxE,MAAI,WAAY,QAAO;AAGvB,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,UAAU,KAAK,WAAW,YAAY,IAAI,UAAU;AAC1D,QAAM,UAAU,kBAAkB,OAAO;AACzC,MAAI,QAAS,QAAO;AAGpB,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/i18n/resolve-locale.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies, headers } from 'next/headers';\nimport { DEFAULT_LOCALE, Locale, LOCALE_COOKIE } from './config';\nimport { localeFromCountry } from './country-language';\nimport { normalizeLocale } from './normalize';\nimport { decodeJwtPayload } from '../utils/jwt-payload';\n\nconst AUTH_COOKIE = 'greatapps';\nconst GEO_HEADER = 'cf-ipcountry';\nconst DEFAULT_WHITELABEL_ID = 1;\n\ninterface JwtLocaleInfo {\n id_wl?: number;\n country?: string | null;\n}\n\n/** Lê id_wl e país do JWT sem custo de rede (best-effort). */\nfunction decodeJwtLocaleInfo(token?: string | null): JwtLocaleInfo | null {\n if (!token) return null;\n try {\n const payload = decodeJwtPayload<{ id_wl?: number | string; session?: { location?: { country?: string | null } } }>(token);\n return {\n id_wl: payload?.id_wl != null ? Number(payload.id_wl) : undefined,\n country: payload?.session?.location?.country ?? null,\n };\n } catch {\n return null;\n }\n}\n\nexport interface ResolveLocaleOptions {\n /**\n * Whitelabel id já resolvido (ex.: via `findWhitelabel()`), com prioridade sobre o JWT.\n * Permite detectar domínio whitelabel mesmo para visitante não-logado.\n */\n whitelabelId?: number | null;\n /** Idioma da conta (`user.language`), quando já conhecido pelo chamador. */\n userLanguage?: string | null;\n}\n\n/**\n * Resolve o locale da requisição (server-side). Ordem de prioridade:\n *\n * 1. **Whitelabel não-padrão** (`id_wl != 1`) → `pt` (por enquanto, whitelabels não são localizadas).\n * 2. **Preferência da conta** (`userLanguage`), quando fornecida.\n * 3. **Cookie `NEXT_LOCALE`** — cache da preferência ou escolha manual do visitante.\n * 4. **Inferência por país** — JWT (`session.location.country`) ou header `CF-IPCountry`.\n * 5. **Fallback** → {@link DEFAULT_LOCALE}.\n */\nexport async function resolveLocale(options: ResolveLocaleOptions = {}): Promise<Locale> {\n const cookieStore = await cookies();\n const token = process.env.DUMMY_AUTH_TOKEN || cookieStore.get(AUTH_COOKIE)?.value;\n const jwt = decodeJwtLocaleInfo(token);\n\n // 1. Whitelabel não-padrão → português\n const whitelabelId = options.whitelabelId ?? jwt?.id_wl ?? null;\n if (whitelabelId != null && whitelabelId !== DEFAULT_WHITELABEL_ID) {\n return 'pt-br';\n }\n\n // 2. Preferência explícita da conta\n const fromUser = normalizeLocale(options.userLanguage);\n if (fromUser) return fromUser;\n\n // 3. Cookie persistido\n const fromCookie = normalizeLocale(cookieStore.get(LOCALE_COOKIE)?.value);\n if (fromCookie) return fromCookie;\n\n // 4. Inferência por país (geo)\n const headersList = await headers();\n const country = jwt?.country ?? headersList.get(GEO_HEADER);\n const fromGeo = localeFromCountry(country);\n if (fromGeo) return fromGeo;\n\n // 5. Fallback\n return DEFAULT_LOCALE;\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,SAAS,eAAe;AACjC,SAAS,gBAAwB,qBAAqB;AACtD,SAAS,yBAAyB;AAClC,SAAS,uBAAuB;AAChC,SAAS,wBAAwB;AAEjC,MAAM,cAAc;AACpB,MAAM,aAAa;AACnB,MAAM,wBAAwB;AAQ9B,SAAS,oBAAoB,OAA6C;AACxE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,UAAU,iBAAoG,KAAK;AACzH,WAAO;AAAA,MACL,OAAO,SAAS,SAAS,OAAO,OAAO,QAAQ,KAAK,IAAI;AAAA,MACxD,SAAS,SAAS,SAAS,UAAU,WAAW;AAAA,IAClD;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAqBA,eAAsB,cAAc,UAAgC,CAAC,GAAoB;AACvF,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,QAAQ,IAAI,oBAAoB,YAAY,IAAI,WAAW,GAAG;AAC5E,QAAM,MAAM,oBAAoB,KAAK;AAGrC,QAAM,eAAe,QAAQ,gBAAgB,KAAK,SAAS;AAC3D,MAAI,gBAAgB,QAAQ,iBAAiB,uBAAuB;AAClE,WAAO;AAAA,EACT;AAGA,QAAM,WAAW,gBAAgB,QAAQ,YAAY;AACrD,MAAI,SAAU,QAAO;AAGrB,QAAM,aAAa,gBAAgB,YAAY,IAAI,aAAa,GAAG,KAAK;AACxE,MAAI,WAAY,QAAO;AAGvB,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,UAAU,KAAK,WAAW,YAAY,IAAI,UAAU;AAC1D,QAAM,UAAU,kBAAkB,OAAO;AACzC,MAAI,QAAS,QAAO;AAGpB,SAAO;AACT;","names":[]}
@@ -1,4 +1,5 @@
1
1
  import "server-only";
2
+ import { decodeJwtPayload } from "../../../utils/jwt-payload";
2
3
  import { findWhitelabel } from "../../whitelabel/actions/find-whitelabel.action";
3
4
  import { whitelabelService } from "../../whitelabel/services/whitelabel.service";
4
5
  async function buildWlOverride(jwtWlId) {
@@ -14,7 +15,7 @@ async function buildWlOverride(jwtWlId) {
14
15
  }
15
16
  async function buildWlOverrideFromJwt(cookie) {
16
17
  try {
17
- const payload = JSON.parse(atob(cookie.split(".")[1]));
18
+ const payload = decodeJwtPayload(cookie);
18
19
  return buildWlOverride(Number(payload.id_wl));
19
20
  } catch {
20
21
  return void 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/auth/utils/build-wl-override.ts"],"sourcesContent":["import 'server-only';\n\nimport type { RequestConfig } from '../../../infra/api/client';\nimport type { JWTPayload } from '../../users/schema';\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\nimport { whitelabelService } from '../../whitelabel/services/whitelabel.service';\n\n/**\n * Compara um `id_wl` com o whitelabel resolvido pelo hostname.\n * Se forem diferentes (ex: admin com id_wl=1 num domínio de id_wl=30),\n * retorna um RequestConfig com o override correto.\n * Se forem iguais, retorna undefined (sem custo extra).\n */\nexport async function buildWlOverride(\n jwtWlId: number,\n): Promise<RequestConfig | undefined> {\n try {\n const { id: hostWlId } = await findWhitelabel();\n\n if (jwtWlId !== hostWlId) {\n const wlToken = await whitelabelService.getTokenByWhitelabelId(jwtWlId);\n return { whiteLabelId: jwtWlId, authToken: wlToken };\n }\n } catch {\n // Fallback to default whitelabel resolution\n }\n\n return undefined;\n}\n\n/**\n * Extrai `id_wl` do JWT cookie e delega para `buildWlOverride`.\n */\nexport async function buildWlOverrideFromJwt(\n cookie: string,\n): Promise<RequestConfig | undefined> {\n try {\n const payload = JSON.parse(atob(cookie.split('.')[1])) as JWTPayload;\n return buildWlOverride(Number(payload.id_wl));\n } catch {\n return undefined;\n }\n}\n"],"mappings":"AAAA,OAAO;AAIP,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAQlC,eAAsB,gBACpB,SACoC;AACpC,MAAI;AACF,UAAM,EAAE,IAAI,SAAS,IAAI,MAAM,eAAe;AAE9C,QAAI,YAAY,UAAU;AACxB,YAAM,UAAU,MAAM,kBAAkB,uBAAuB,OAAO;AACtE,aAAO,EAAE,cAAc,SAAS,WAAW,QAAQ;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAKA,eAAsB,uBACpB,QACoC;AACpC,MAAI;AACF,UAAM,UAAU,KAAK,MAAM,KAAK,OAAO,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC;AACrD,WAAO,gBAAgB,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/auth/utils/build-wl-override.ts"],"sourcesContent":["import 'server-only';\n\nimport type { RequestConfig } from '../../../infra/api/client';\nimport type { JWTPayload } from '../../users/schema';\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\nimport { whitelabelService } from '../../whitelabel/services/whitelabel.service';\n\n/**\n * Compara um `id_wl` com o whitelabel resolvido pelo hostname.\n * Se forem diferentes (ex: admin com id_wl=1 num domínio de id_wl=30),\n * retorna um RequestConfig com o override correto.\n * Se forem iguais, retorna undefined (sem custo extra).\n */\nexport async function buildWlOverride(\n jwtWlId: number,\n): Promise<RequestConfig | undefined> {\n try {\n const { id: hostWlId } = await findWhitelabel();\n\n if (jwtWlId !== hostWlId) {\n const wlToken = await whitelabelService.getTokenByWhitelabelId(jwtWlId);\n return { whiteLabelId: jwtWlId, authToken: wlToken };\n }\n } catch {\n // Fallback to default whitelabel resolution\n }\n\n return undefined;\n}\n\n/**\n * Extrai `id_wl` do JWT cookie e delega para `buildWlOverride`.\n */\nexport async function buildWlOverrideFromJwt(\n cookie: string,\n): Promise<RequestConfig | undefined> {\n try {\n const payload = decodeJwtPayload<JWTPayload>(cookie);\n return buildWlOverride(Number(payload.id_wl));\n } catch {\n return undefined;\n }\n}\n"],"mappings":"AAAA,OAAO;AAIP,SAAS,wBAAwB;AACjC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAQlC,eAAsB,gBACpB,SACoC;AACpC,MAAI;AACF,UAAM,EAAE,IAAI,SAAS,IAAI,MAAM,eAAe;AAE9C,QAAI,YAAY,UAAU;AACxB,YAAM,UAAU,MAAM,kBAAkB,uBAAuB,OAAO;AACtE,aAAO,EAAE,cAAc,SAAS,WAAW,QAAQ;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAKA,eAAsB,uBACpB,QACoC;AACpC,MAAI;AACF,UAAM,UAAU,iBAA6B,MAAM;AACnD,WAAO,gBAAgB,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,6 +1,7 @@
1
1
  import "server-only";
2
2
  import { cookies } from "next/headers";
3
3
  import { ApiError } from "../../../infra/api/types";
4
+ import { decodeJwtPayload } from "../../../utils/jwt-payload";
4
5
  const AUTH_COOKIE_NAME = "greatapps";
5
6
  async function getUserContext() {
6
7
  const cookieStore = await cookies();
@@ -9,7 +10,7 @@ async function getUserContext() {
9
10
  throw new ApiError("Usu\xE1rio n\xE3o autenticado", "NOT_AUTHENTICATED_GUC", 401);
10
11
  }
11
12
  try {
12
- const payload = JSON.parse(atob(token.split(".")[1]));
13
+ const payload = decodeJwtPayload(token);
13
14
  return {
14
15
  id_wl: Number(payload.id_wl),
15
16
  id_account: payload.id_account,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/auth/utils/get-user-context.ts"],"sourcesContent":["import 'server-only';\r\n\r\nimport { cookies } from 'next/headers';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { JWTPayload } from '../../users/schema';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\n\r\nexport async function getUserContext(): Promise<{\r\n id_wl: number;\r\n id_account: number;\r\n id_user: number;\r\n}> {\r\n const cookieStore = await cookies();\r\n const token = process.env.DUMMY_AUTH_TOKEN || cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Usuário não autenticado', 'NOT_AUTHENTICATED_GUC', 401);\r\n }\r\n\r\n try {\r\n const payload = JSON.parse(atob(token.split('.')[1])) as JWTPayload;\r\n\r\n return {\r\n id_wl: Number(payload.id_wl),\r\n id_account: payload.id_account,\r\n id_user: payload.id_user,\r\n };\r\n } catch {\r\n throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);\r\n }\r\n}\r\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAGzB,MAAM,mBAAmB;AAEzB,eAAsB,iBAInB;AACD,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,QAAQ,IAAI,oBAAoB,YAAY,IAAI,gBAAgB,GAAG;AAEjF,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,SAAS,iCAA2B,yBAAyB,GAAG;AAAA,EAC5E;AAEA,MAAI;AACF,UAAM,UAAU,KAAK,MAAM,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC;AAEpD,WAAO;AAAA,MACL,OAAO,OAAO,QAAQ,KAAK;AAAA,MAC3B,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,IACnB;AAAA,EACF,QAAQ;AACN,UAAM,IAAI,SAAS,qBAAkB,iBAAiB,GAAG;AAAA,EAC3D;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/auth/utils/get-user-context.ts"],"sourcesContent":["import 'server-only';\r\n\r\nimport { cookies } from 'next/headers';\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { JWTPayload } from '../../users/schema';\r\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\n\r\nexport async function getUserContext(): Promise<{\r\n id_wl: number;\r\n id_account: number;\r\n id_user: number;\r\n}> {\r\n const cookieStore = await cookies();\r\n const token = process.env.DUMMY_AUTH_TOKEN || cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Usuário não autenticado', 'NOT_AUTHENTICATED_GUC', 401);\r\n }\r\n\r\n try {\r\n const payload = decodeJwtPayload<JWTPayload>(token);\r\n\r\n return {\r\n id_wl: Number(payload.id_wl),\r\n id_account: payload.id_account,\r\n id_user: payload.id_user,\r\n };\r\n } catch {\r\n throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);\r\n }\r\n}\r\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAEzB,SAAS,wBAAwB;AAEjC,MAAM,mBAAmB;AAEzB,eAAsB,iBAInB;AACD,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,QAAQ,IAAI,oBAAoB,YAAY,IAAI,gBAAgB,GAAG;AAEjF,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,SAAS,iCAA2B,yBAAyB,GAAG;AAAA,EAC5E;AAEA,MAAI;AACF,UAAM,UAAU,iBAA6B,KAAK;AAElD,WAAO;AAAA,MACL,OAAO,OAAO,QAAQ,KAAK;AAAA,MAC3B,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,IACnB;AAAA,EACF,QAAQ;AACN,UAAM,IAAI,SAAS,qBAAkB,iBAAiB,GAAG;AAAA,EAC3D;AACF;","names":[]}
@@ -2,6 +2,7 @@ import "server-only";
2
2
  import { cookies } from "next/headers";
3
3
  import { decode } from "@greatapps/jwt";
4
4
  import { ApiError } from "../../../infra/api/types";
5
+ import { decodeJwtPayload } from "../../../utils/jwt-payload";
5
6
  const AUTH_COOKIE_NAME = "greatapps";
6
7
  async function requireTokenNotExpired() {
7
8
  if (process.env.DUMMY_AUTH_TOKEN) return;
@@ -13,7 +14,7 @@ async function requireTokenNotExpired() {
13
14
  throw new ApiError("Sess\xE3o inv\xE1lida ou expirada 1", "SESSION_INVALID", 401);
14
15
  }
15
16
  try {
16
- const payload = JSON.parse(atob(token.split(".")[1]));
17
+ const payload = decodeJwtPayload(token);
17
18
  const idWl = payload.id_wl;
18
19
  if (!idWl) {
19
20
  throw new ApiError("Sess\xE3o inv\xE1lida ou expirada 2", "SESSION_INVALID", 401);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/auth/utils/require-token-not-expired.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies } from 'next/headers';\nimport { decode } from '@greatapps/jwt';\nimport { ApiError } from '../../../infra/api/types';\n\nconst AUTH_COOKIE_NAME = 'greatapps';\n\n/**\n * Verifica se o JWT do cookie `greatapps` é válido (assinatura + expiração)\n * sem bater no backend.\n *\n * Usa `@greatapps/jwt` decode que:\n * 1. Valida a assinatura HMAC com `SECRET_JWT---{id_wl}`\n * 2. Checa `exp` automaticamente (rejeita se expirou)\n *\n * - Se o token não existe → SESSION_INVALID 401\n * - Se a assinatura é inválida → SESSION_INVALID 401\n * - Se `exp` já passou → SESSION_INVALID 401\n * - Se `SECRET_JWT` não está configurado → skip (não bloqueia)\n */\nexport async function requireTokenNotExpired(): Promise<void> {\n if (process.env.DUMMY_AUTH_TOKEN) return;\n\n const secretJwt = process.env.SECRET_JWT;\n if (!secretJwt) return;\n\n const cookieStore = await cookies();\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\n\n if (!token) {\n throw new ApiError('Sessão inválida ou expirada 1', 'SESSION_INVALID', 401);\n }\n\n try {\n const payload = JSON.parse(atob(token.split('.')[1]));\n const idWl = payload.id_wl;\n\n if (!idWl) {\n throw new ApiError('Sessão inválida ou expirada 2', 'SESSION_INVALID', 401);\n }\n\n const secret = `${secretJwt}---${idWl}`;\n const decoded = await decode(token, secret);\n\n if (!decoded) {\n throw new ApiError('Sessão inválida ou expirada 3', 'SESSION_INVALID', 401);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n throw new ApiError('Sessão inválida ou expirada 4', 'SESSION_INVALID', 401);\n }\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AAEzB,MAAM,mBAAmB;AAezB,eAAsB,yBAAwC;AAC5D,MAAI,QAAQ,IAAI,iBAAkB;AAElC,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,UAAW;AAEhB,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,YAAY,IAAI,gBAAgB,GAAG;AAEjD,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,EAC5E;AAEA,MAAI;AACF,UAAM,UAAU,KAAK,MAAM,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC;AACpD,UAAM,OAAO,QAAQ;AAErB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,IAC5E;AAEA,UAAM,SAAS,GAAG,SAAS,MAAM,IAAI;AACrC,UAAM,UAAU,MAAM,OAAO,OAAO,MAAM;AAE1C,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,IAC5E;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,eAAe,SAAU,OAAM;AACnC,UAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,EAC5E;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/auth/utils/require-token-not-expired.ts"],"sourcesContent":["import 'server-only';\n\nimport { cookies } from 'next/headers';\nimport { decode } from '@greatapps/jwt';\nimport { ApiError } from '../../../infra/api/types';\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\n\nconst AUTH_COOKIE_NAME = 'greatapps';\n\n/**\n * Verifica se o JWT do cookie `greatapps` é válido (assinatura + expiração)\n * sem bater no backend.\n *\n * Usa `@greatapps/jwt` decode que:\n * 1. Valida a assinatura HMAC com `SECRET_JWT---{id_wl}`\n * 2. Checa `exp` automaticamente (rejeita se expirou)\n *\n * - Se o token não existe → SESSION_INVALID 401\n * - Se a assinatura é inválida → SESSION_INVALID 401\n * - Se `exp` já passou → SESSION_INVALID 401\n * - Se `SECRET_JWT` não está configurado → skip (não bloqueia)\n */\nexport async function requireTokenNotExpired(): Promise<void> {\n if (process.env.DUMMY_AUTH_TOKEN) return;\n\n const secretJwt = process.env.SECRET_JWT;\n if (!secretJwt) return;\n\n const cookieStore = await cookies();\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\n\n if (!token) {\n throw new ApiError('Sessão inválida ou expirada 1', 'SESSION_INVALID', 401);\n }\n\n try {\n const payload = decodeJwtPayload<{ id_wl?: number | string }>(token);\n const idWl = payload.id_wl;\n\n if (!idWl) {\n throw new ApiError('Sessão inválida ou expirada 2', 'SESSION_INVALID', 401);\n }\n\n const secret = `${secretJwt}---${idWl}`;\n const decoded = await decode(token, secret);\n\n if (!decoded) {\n throw new ApiError('Sessão inválida ou expirada 3', 'SESSION_INVALID', 401);\n }\n } catch (err) {\n if (err instanceof ApiError) throw err;\n throw new ApiError('Sessão inválida ou expirada 4', 'SESSION_INVALID', 401);\n }\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AAEjC,MAAM,mBAAmB;AAezB,eAAsB,yBAAwC;AAC5D,MAAI,QAAQ,IAAI,iBAAkB;AAElC,QAAM,YAAY,QAAQ,IAAI;AAC9B,MAAI,CAAC,UAAW;AAEhB,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,YAAY,IAAI,gBAAgB,GAAG;AAEjD,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,EAC5E;AAEA,MAAI;AACF,UAAM,UAAU,iBAA8C,KAAK;AACnE,UAAM,OAAO,QAAQ;AAErB,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,IAC5E;AAEA,UAAM,SAAS,GAAG,SAAS,MAAM,IAAI;AACrC,UAAM,UAAU,MAAM,OAAO,OAAO,MAAM;AAE1C,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,IAC5E;AAAA,EACF,SAAS,KAAK;AACZ,QAAI,eAAe,SAAU,OAAM;AACnC,UAAM,IAAI,SAAS,uCAAiC,mBAAmB,GAAG;AAAA,EAC5E;AACF;","names":[]}
@@ -15,6 +15,7 @@ import {
15
15
  import {
16
16
  SubscriptionPendingPixResponseSchema
17
17
  } from "../types/pix-pending.type";
18
+ const SUBSCRIPTION_WRITE_TIMEOUT_MS = 6e4;
18
19
  class SubscriptionsService {
19
20
  async listSubscriptions(params) {
20
21
  const { id_account } = await getUserContext();
@@ -61,7 +62,8 @@ class SubscriptionsService {
61
62
  const { id_account } = await getUserContext();
62
63
  const response = await api.apps.put(
63
64
  `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,
64
- data
65
+ data,
66
+ { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS }
65
67
  );
66
68
  if (response.status === 0) {
67
69
  throw new ApiError(
@@ -87,7 +89,8 @@ class SubscriptionsService {
87
89
  const { id_account } = await getUserContext();
88
90
  const response = await api.apps.put(
89
91
  `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,
90
- data
92
+ data,
93
+ { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS }
91
94
  );
92
95
  if (response.status === 0) {
93
96
  throw new ApiError(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/subscriptions/services/subscriptions.service.ts"],"sourcesContent":["import \"server-only\";\r\n\r\nimport { api } from \"../../../infra/api/client\";\r\nimport {\r\n ApiError,\r\n ApiActionResult,\r\n ApiPaginatedActionResult,\r\n PaginatedSuccessResult,\r\n SuccessResult,\r\n} from \"../../../infra/api/types\";\r\nimport { buildQueryParams } from \"../../../infra/utils/params\";\r\nimport { assertManagementPermission } from \"../../auth/utils/assert-management-permission\";\r\nimport { getUserContext } from \"../../auth/utils/get-user-context\";\r\nimport {\r\n Subscription,\r\n SubscriptionSchema,\r\n FindSubscriptionsParams,\r\n} from \"../types/subscription.type\";\r\nimport {\r\n type CalculateSubscriptionRequest,\r\n type CalculateSubscriptionResponse,\r\n type UpdateSubscriptionPlanRequest,\r\n CalculateSubscriptionResponseSchema,\r\n} from \"../types/calculate-subscription.type\";\r\nimport {\r\n SubscriptionPendingPixResponseSchema,\r\n type SubscriptionPendingPixResponse,\r\n} from \"../types/pix-pending.type\";\r\n\r\nexport type UpdateSubscriptionPlanResponse =\r\n | Subscription\r\n | SubscriptionPendingPixResponse;\r\n\r\nclass SubscriptionsService {\r\n async listSubscriptions(\r\n params?: Partial<FindSubscriptionsParams>,\r\n ): Promise<PaginatedSuccessResult<Subscription>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const query = buildQueryParams(\r\n params as Record<string, string | boolean | undefined>,\r\n );\r\n const url = `/accounts/${id_account}/subscriptions${query ? `?${query}` : \"\"}`;\r\n\r\n const response =\r\n await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao listar assinaturas\",\r\n \"LIST_SUBSCRIPTIONS_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const data = response.data.map((item) => SubscriptionSchema.parse(item));\r\n\r\n return {\r\n data,\r\n total: response.total,\r\n success: true,\r\n } satisfies PaginatedSuccessResult<Subscription>;\r\n }\r\n\r\n async calculateSubscription(\r\n data: CalculateSubscriptionRequest,\r\n ): Promise<SuccessResult<CalculateSubscriptionResponse>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.post<\r\n ApiActionResult<CalculateSubscriptionResponse>\r\n >(`/accounts/${id_account}/subscriptions/action/calculate`, data);\r\n\r\n console.log(\"Calculate subscription response:\", {\r\n response,\r\n data: JSON.stringify(data, null, 2),\r\n });\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao calcular assinatura\",\r\n \"CALCULATE_SUBSCRIPTION_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return {\r\n success: true,\r\n data: CalculateSubscriptionResponseSchema.parse(response.data),\r\n };\r\n }\r\n\r\n async updateSubscriptionPlan(\r\n subscriptionId: number | string,\r\n data: UpdateSubscriptionPlanRequest,\r\n ): Promise<\r\n SuccessResult<UpdateSubscriptionPlanResponse | null> & { client_secret?: string }\r\n > {\r\n await assertManagementPermission('manage_subscriptions');\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<\r\n ApiActionResult<UpdateSubscriptionPlanResponse | UpdateSubscriptionPlanResponse[]> & {\r\n client_secret?: string;\r\n }\r\n >(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,\r\n data,\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar plano\",\r\n \"UPDATE_SUBSCRIPTION_PLAN_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const raw = Array.isArray(response.data) ? response.data[0] : response.data;\r\n\r\n // Preserva resposta pix-pending (id: null, pending: true, pix: {...})\r\n // pra clientes poderem rotear o user pra fluxo dedicado de pix em vez\r\n // de tratar como subscription comum. Caso contrário, parseia como\r\n // Subscription (compatível com o comportamento antigo de devolver { id }).\r\n let parsed: UpdateSubscriptionPlanResponse | null = null;\r\n if (raw) {\r\n const pixResult = SubscriptionPendingPixResponseSchema.safeParse(raw);\r\n parsed = pixResult.success ? pixResult.data : SubscriptionSchema.parse(raw);\r\n }\r\n\r\n return {\r\n success: true,\r\n data: parsed,\r\n ...(response.client_secret ? { client_secret: response.client_secret } : {}),\r\n };\r\n }\r\n\r\n async updateSubscriptionPayment(\r\n subscriptionId: number | string,\r\n data: { id_card: string; payment_method: number },\r\n ): Promise<SuccessResult<void>> {\r\n await assertManagementPermission(\"manage_subscriptions\");\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<ApiActionResult<void>>(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,\r\n data,\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar método de pagamento\",\r\n \"UPDATE_SUBSCRIPTION_PAYMENT_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return { success: true };\r\n }\r\n}\r\n\r\nexport const subscriptionsService = new SubscriptionsService();\r\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,WAAW;AACpB;AAAA,EACE;AAAA,OAKK;AACP,SAAS,wBAAwB;AACjC,SAAS,kCAAkC;AAC3C,SAAS,sBAAsB;AAC/B;AAAA,EAEE;AAAA,OAEK;AACP;AAAA,EAIE;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAEK;AAMP,MAAM,qBAAqB;AAAA,EACzB,MAAM,kBACJ,QAC+C;AAC/C,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,QAAQ;AAAA,MACZ;AAAA,IACF;AACA,UAAM,MAAM,aAAa,UAAU,iBAAiB,QAAQ,IAAI,KAAK,KAAK,EAAE;AAE5E,UAAM,WACJ,MAAM,IAAI,KAAK,IAA4C,GAAG;AAEhE,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,SAAS,KAAK,IAAI,CAAC,SAAS,mBAAmB,MAAM,IAAI,CAAC;AAEvE,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,sBACJ,MACuD;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK,KAE9B,aAAa,UAAU,mCAAmC,IAAI;AAEhE,YAAQ,IAAI,oCAAoC;AAAA,MAC9C;AAAA,MACA,MAAM,KAAK,UAAU,MAAM,MAAM,CAAC;AAAA,IACpC,CAAC;AAED,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,oCAAoC,MAAM,SAAS,IAAI;AAAA,IAC/D;AAAA,EACF;AAAA,EAEA,MAAM,uBACJ,gBACA,MAGA;AACA,UAAM,2BAA2B,sBAAsB;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAK9B,aAAa,UAAU,kBAAkB,cAAc;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,MAAM,MAAM,QAAQ,SAAS,IAAI,IAAI,SAAS,KAAK,CAAC,IAAI,SAAS;AAMvE,QAAI,SAAgD;AACpD,QAAI,KAAK;AACP,YAAM,YAAY,qCAAqC,UAAU,GAAG;AACpE,eAAS,UAAU,UAAU,UAAU,OAAO,mBAAmB,MAAM,GAAG;AAAA,IAC5E;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,GAAI,SAAS,gBAAgB,EAAE,eAAe,SAAS,cAAc,IAAI,CAAC;AAAA,IAC5E;AAAA,EACF;AAAA,EAEA,MAAM,0BACJ,gBACA,MAC8B;AAC9B,UAAM,2BAA2B,sBAAsB;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAC9B,aAAa,UAAU,kBAAkB,cAAc;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACF;AAEO,MAAM,uBAAuB,IAAI,qBAAqB;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/services/subscriptions.service.ts"],"sourcesContent":["import \"server-only\";\r\n\r\nimport { api } from \"../../../infra/api/client\";\r\nimport {\r\n ApiError,\r\n ApiActionResult,\r\n ApiPaginatedActionResult,\r\n PaginatedSuccessResult,\r\n SuccessResult,\r\n} from \"../../../infra/api/types\";\r\nimport { buildQueryParams } from \"../../../infra/utils/params\";\r\nimport { assertManagementPermission } from \"../../auth/utils/assert-management-permission\";\r\nimport { getUserContext } from \"../../auth/utils/get-user-context\";\r\nimport {\r\n Subscription,\r\n SubscriptionSchema,\r\n FindSubscriptionsParams,\r\n} from \"../types/subscription.type\";\r\nimport {\r\n type CalculateSubscriptionRequest,\r\n type CalculateSubscriptionResponse,\r\n type UpdateSubscriptionPlanRequest,\r\n CalculateSubscriptionResponseSchema,\r\n} from \"../types/calculate-subscription.type\";\r\nimport {\r\n SubscriptionPendingPixResponseSchema,\r\n type SubscriptionPendingPixResponse,\r\n} from \"../types/pix-pending.type\";\r\n\r\n/**\r\n * Timeout das mutations de assinatura que ESCREVEM (action/plan e action/payment).\r\n *\r\n * O default do ApiClient é 30s. Essas duas rotas caem no mesmo motor pesado do\r\n * backend (planEdit delega pra paymentEdit) — troca de método de pagamento, mandato\r\n * PIX recorrente Woovi, void-and-replace de fatura — e passam de 30s com alguma\r\n * frequência: em produção elas aparecem como `outcome=canceled` com wallTime ~29.9s.\r\n *\r\n * O problema não é o usuário ver \"Tempo excedido\": é que o abort MATA o worker no\r\n * meio da saga. Se o corte cai depois de criar o mandato Woovi e suprimir a fatura\r\n * de ciclo, sobra mandato órfão + fatura sem dunning, o usuário não recebe QR nenhum\r\n * e a próxima tentativa cria um segundo mandato. Deixar a request terminar converte\r\n * uma escrita parcial numa escrita completa.\r\n *\r\n * Não é a solução definitiva — a rota deveria ser assíncrona (202 + polling no\r\n * correlation_id). É o teto que dá folga enquanto isso.\r\n *\r\n * Só nas mutations: calculateSubscription é idempotente (não escreve nada), então\r\n * um timeout nela não deixa rastro e não precisa esperar mais.\r\n */\r\nconst SUBSCRIPTION_WRITE_TIMEOUT_MS = 60000;\r\n\r\nexport type UpdateSubscriptionPlanResponse =\r\n | Subscription\r\n | SubscriptionPendingPixResponse;\r\n\r\nclass SubscriptionsService {\r\n async listSubscriptions(\r\n params?: Partial<FindSubscriptionsParams>,\r\n ): Promise<PaginatedSuccessResult<Subscription>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const query = buildQueryParams(\r\n params as Record<string, string | boolean | undefined>,\r\n );\r\n const url = `/accounts/${id_account}/subscriptions${query ? `?${query}` : \"\"}`;\r\n\r\n const response =\r\n await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao listar assinaturas\",\r\n \"LIST_SUBSCRIPTIONS_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const data = response.data.map((item) => SubscriptionSchema.parse(item));\r\n\r\n return {\r\n data,\r\n total: response.total,\r\n success: true,\r\n } satisfies PaginatedSuccessResult<Subscription>;\r\n }\r\n\r\n async calculateSubscription(\r\n data: CalculateSubscriptionRequest,\r\n ): Promise<SuccessResult<CalculateSubscriptionResponse>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.post<\r\n ApiActionResult<CalculateSubscriptionResponse>\r\n >(`/accounts/${id_account}/subscriptions/action/calculate`, data);\r\n\r\n console.log(\"Calculate subscription response:\", {\r\n response,\r\n data: JSON.stringify(data, null, 2),\r\n });\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao calcular assinatura\",\r\n \"CALCULATE_SUBSCRIPTION_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return {\r\n success: true,\r\n data: CalculateSubscriptionResponseSchema.parse(response.data),\r\n };\r\n }\r\n\r\n async updateSubscriptionPlan(\r\n subscriptionId: number | string,\r\n data: UpdateSubscriptionPlanRequest,\r\n ): Promise<\r\n SuccessResult<UpdateSubscriptionPlanResponse | null> & { client_secret?: string }\r\n > {\r\n await assertManagementPermission('manage_subscriptions');\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<\r\n ApiActionResult<UpdateSubscriptionPlanResponse | UpdateSubscriptionPlanResponse[]> & {\r\n client_secret?: string;\r\n }\r\n >(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar plano\",\r\n \"UPDATE_SUBSCRIPTION_PLAN_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const raw = Array.isArray(response.data) ? response.data[0] : response.data;\r\n\r\n // Preserva resposta pix-pending (id: null, pending: true, pix: {...})\r\n // pra clientes poderem rotear o user pra fluxo dedicado de pix em vez\r\n // de tratar como subscription comum. Caso contrário, parseia como\r\n // Subscription (compatível com o comportamento antigo de devolver { id }).\r\n let parsed: UpdateSubscriptionPlanResponse | null = null;\r\n if (raw) {\r\n const pixResult = SubscriptionPendingPixResponseSchema.safeParse(raw);\r\n parsed = pixResult.success ? pixResult.data : SubscriptionSchema.parse(raw);\r\n }\r\n\r\n return {\r\n success: true,\r\n data: parsed,\r\n ...(response.client_secret ? { client_secret: response.client_secret } : {}),\r\n };\r\n }\r\n\r\n async updateSubscriptionPayment(\r\n subscriptionId: number | string,\r\n data: { id_card: string; payment_method: number },\r\n ): Promise<SuccessResult<void>> {\r\n await assertManagementPermission(\"manage_subscriptions\");\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<ApiActionResult<void>>(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar método de pagamento\",\r\n \"UPDATE_SUBSCRIPTION_PAYMENT_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return { success: true };\r\n }\r\n}\r\n\r\nexport const subscriptionsService = new SubscriptionsService();\r\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,WAAW;AACpB;AAAA,EACE;AAAA,OAKK;AACP,SAAS,wBAAwB;AACjC,SAAS,kCAAkC;AAC3C,SAAS,sBAAsB;AAC/B;AAAA,EAEE;AAAA,OAEK;AACP;AAAA,EAIE;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAEK;AAsBP,MAAM,gCAAgC;AAMtC,MAAM,qBAAqB;AAAA,EACzB,MAAM,kBACJ,QAC+C;AAC/C,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,QAAQ;AAAA,MACZ;AAAA,IACF;AACA,UAAM,MAAM,aAAa,UAAU,iBAAiB,QAAQ,IAAI,KAAK,KAAK,EAAE;AAE5E,UAAM,WACJ,MAAM,IAAI,KAAK,IAA4C,GAAG;AAEhE,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,SAAS,KAAK,IAAI,CAAC,SAAS,mBAAmB,MAAM,IAAI,CAAC;AAEvE,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,sBACJ,MACuD;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK,KAE9B,aAAa,UAAU,mCAAmC,IAAI;AAEhE,YAAQ,IAAI,oCAAoC;AAAA,MAC9C;AAAA,MACA,MAAM,KAAK,UAAU,MAAM,MAAM,CAAC;AAAA,IACpC,CAAC;AAED,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,oCAAoC,MAAM,SAAS,IAAI;AAAA,IAC/D;AAAA,EACF;AAAA,EAEA,MAAM,uBACJ,gBACA,MAGA;AACA,UAAM,2BAA2B,sBAAsB;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAK9B,aAAa,UAAU,kBAAkB,cAAc;AAAA,MACvD;AAAA,MACA,EAAE,SAAS,8BAA8B;AAAA,IAC3C;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,MAAM,MAAM,QAAQ,SAAS,IAAI,IAAI,SAAS,KAAK,CAAC,IAAI,SAAS;AAMvE,QAAI,SAAgD;AACpD,QAAI,KAAK;AACP,YAAM,YAAY,qCAAqC,UAAU,GAAG;AACpE,eAAS,UAAU,UAAU,UAAU,OAAO,mBAAmB,MAAM,GAAG;AAAA,IAC5E;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,GAAI,SAAS,gBAAgB,EAAE,eAAe,SAAS,cAAc,IAAI,CAAC;AAAA,IAC5E;AAAA,EACF;AAAA,EAEA,MAAM,0BACJ,gBACA,MAC8B;AAC9B,UAAM,2BAA2B,sBAAsB;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAC9B,aAAa,UAAU,kBAAkB,cAAc;AAAA,MACvD;AAAA,MACA,EAAE,SAAS,8BAA8B;AAAA,IAC3C;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACF;AAEO,MAAM,uBAAuB,IAAI,qBAAqB;","names":[]}
@@ -1,5 +1,6 @@
1
1
  import { cookies } from "next/headers";
2
2
  import { ApiError } from "../../../infra/api/types";
3
+ import { decodeJwtPayload } from "../../../utils/jwt-payload";
3
4
  import { findWhitelabel } from "../../whitelabel/actions/find-whitelabel.action";
4
5
  import { buildWlOverride } from "../../auth/utils/build-wl-override";
5
6
  const AUTH_COOKIE_NAME = "greatapps";
@@ -16,7 +17,7 @@ class UserService {
16
17
  }
17
18
  decodeJWT(token) {
18
19
  try {
19
- return JSON.parse(atob(token.split(".")[1]));
20
+ return decodeJwtPayload(token);
20
21
  } catch {
21
22
  throw new ApiError("Token inv\xE1lido", "INVALID_TOKEN", 401);
22
23
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/users/services/user.service.ts"],"sourcesContent":["\r\nimport { cookies } from 'next/headers';\r\n\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { GetUserDataResponse, JWTPayload, User } from '../schema';\r\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\r\nimport { buildWlOverride } from '../../auth/utils/build-wl-override';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\nconst API_BASE_URL = process.env.GAPPS_R3_API_URL || 'https://r3-api.greatapps.dev.br';\r\n\r\nclass UserService {\r\n private async getAuthToken(): Promise<string> {\r\n if (process.env.DUMMY_AUTH_TOKEN) return process.env.DUMMY_AUTH_TOKEN;\r\n const cookieStore = await cookies();\r\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Não autenticado', 'NOT_AUTHENTICATED_GAT', 401);\r\n }\r\n\r\n return token;\r\n }\r\n\r\n private decodeJWT(token: string): JWTPayload {\r\n try {\r\n return JSON.parse(atob(token.split('.')[1])) as JWTPayload;\r\n } catch {\r\n throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);\r\n }\r\n }\r\n\r\n async findById(overrides?: { authToken?: string }): Promise<User> {\r\n const token = await this.getAuthToken();\r\n const payload = this.decodeJWT(token);\r\n\r\n const { id_wl, id_account, id_user } = payload;\r\n const wlOverride = overrides ? undefined : await buildWlOverride(Number(id_wl));\r\n const wlToken = overrides?.authToken ?? wlOverride?.authToken ?? (await findWhitelabel()).token;\r\n\r\n const url = `${API_BASE_URL}/v1/pt-br/${id_wl}/accounts/${id_account}/users/${id_user}`;\r\n\r\n const response = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n authorization: wlToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n throw new ApiError('Erro ao buscar dados do usuário', 'FETCH_ERROR', response.status);\r\n }\r\n\r\n const data = (await response.json()) as GetUserDataResponse;\r\n\r\n if (data.status === 0 || !data.data || data.data.length === 0) {\r\n throw new ApiError('Dados do usuário não encontrados', 'USER_NOT_FOUND', 404);\r\n }\r\n\r\n return data.data[0];\r\n }\r\n}\r\n\r\nexport const userService = new UserService();\r\n"],"mappings":"AACA,SAAS,eAAe;AAExB,SAAS,gBAAgB;AAEzB,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAEhC,MAAM,mBAAmB;AACzB,MAAM,eAAe,QAAQ,IAAI,oBAAoB;AAErD,MAAM,YAAY;AAAA,EAChB,MAAc,eAAgC;AAC5C,QAAI,QAAQ,IAAI,iBAAkB,QAAO,QAAQ,IAAI;AACrD,UAAM,cAAc,MAAM,QAAQ;AAClC,UAAM,QAAQ,YAAY,IAAI,gBAAgB,GAAG;AAEjD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,SAAS,sBAAmB,yBAAyB,GAAG;AAAA,IACpE;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,OAA2B;AAC3C,QAAI;AACF,aAAO,KAAK,MAAM,KAAK,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC;AAAA,IAC7C,QAAQ;AACN,YAAM,IAAI,SAAS,qBAAkB,iBAAiB,GAAG;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,WAAmD;AAChE,UAAM,QAAQ,MAAM,KAAK,aAAa;AACtC,UAAM,UAAU,KAAK,UAAU,KAAK;AAEpC,UAAM,EAAE,OAAO,YAAY,QAAQ,IAAI;AACvC,UAAM,aAAa,YAAY,SAAY,MAAM,gBAAgB,OAAO,KAAK,CAAC;AAC9E,UAAM,UAAU,WAAW,aAAa,YAAY,cAAc,MAAM,eAAe,GAAG;AAE1F,UAAM,MAAM,GAAG,YAAY,aAAa,KAAK,aAAa,UAAU,UAAU,OAAO;AAErF,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,SAAS,sCAAmC,eAAe,SAAS,MAAM;AAAA,IACtF;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAElC,QAAI,KAAK,WAAW,KAAK,CAAC,KAAK,QAAQ,KAAK,KAAK,WAAW,GAAG;AAC7D,YAAM,IAAI,SAAS,0CAAoC,kBAAkB,GAAG;AAAA,IAC9E;AAEA,WAAO,KAAK,KAAK,CAAC;AAAA,EACpB;AACF;AAEO,MAAM,cAAc,IAAI,YAAY;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/users/services/user.service.ts"],"sourcesContent":["\r\nimport { cookies } from 'next/headers';\r\n\r\nimport { ApiError } from '../../../infra/api/types';\r\nimport { decodeJwtPayload } from '../../../utils/jwt-payload';\r\nimport { GetUserDataResponse, JWTPayload, User } from '../schema';\r\nimport { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';\r\nimport { buildWlOverride } from '../../auth/utils/build-wl-override';\r\n\r\nconst AUTH_COOKIE_NAME = 'greatapps';\r\nconst API_BASE_URL = process.env.GAPPS_R3_API_URL || 'https://r3-api.greatapps.dev.br';\r\n\r\nclass UserService {\r\n private async getAuthToken(): Promise<string> {\r\n if (process.env.DUMMY_AUTH_TOKEN) return process.env.DUMMY_AUTH_TOKEN;\r\n const cookieStore = await cookies();\r\n const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;\r\n\r\n if (!token) {\r\n throw new ApiError('Não autenticado', 'NOT_AUTHENTICATED_GAT', 401);\r\n }\r\n\r\n return token;\r\n }\r\n\r\n private decodeJWT(token: string): JWTPayload {\r\n try {\r\n return decodeJwtPayload<JWTPayload>(token);\r\n } catch {\r\n throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);\r\n }\r\n }\r\n\r\n async findById(overrides?: { authToken?: string }): Promise<User> {\r\n const token = await this.getAuthToken();\r\n const payload = this.decodeJWT(token);\r\n\r\n const { id_wl, id_account, id_user } = payload;\r\n const wlOverride = overrides ? undefined : await buildWlOverride(Number(id_wl));\r\n const wlToken = overrides?.authToken ?? wlOverride?.authToken ?? (await findWhitelabel()).token;\r\n\r\n const url = `${API_BASE_URL}/v1/pt-br/${id_wl}/accounts/${id_account}/users/${id_user}`;\r\n\r\n const response = await fetch(url, {\r\n method: 'GET',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n authorization: wlToken,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n throw new ApiError('Erro ao buscar dados do usuário', 'FETCH_ERROR', response.status);\r\n }\r\n\r\n const data = (await response.json()) as GetUserDataResponse;\r\n\r\n if (data.status === 0 || !data.data || data.data.length === 0) {\r\n throw new ApiError('Dados do usuário não encontrados', 'USER_NOT_FOUND', 404);\r\n }\r\n\r\n return data.data[0];\r\n }\r\n}\r\n\r\nexport const userService = new UserService();\r\n"],"mappings":"AACA,SAAS,eAAe;AAExB,SAAS,gBAAgB;AACzB,SAAS,wBAAwB;AAEjC,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAEhC,MAAM,mBAAmB;AACzB,MAAM,eAAe,QAAQ,IAAI,oBAAoB;AAErD,MAAM,YAAY;AAAA,EAChB,MAAc,eAAgC;AAC5C,QAAI,QAAQ,IAAI,iBAAkB,QAAO,QAAQ,IAAI;AACrD,UAAM,cAAc,MAAM,QAAQ;AAClC,UAAM,QAAQ,YAAY,IAAI,gBAAgB,GAAG;AAEjD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,SAAS,sBAAmB,yBAAyB,GAAG;AAAA,IACpE;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,OAA2B;AAC3C,QAAI;AACF,aAAO,iBAA6B,KAAK;AAAA,IAC3C,QAAQ;AACN,YAAM,IAAI,SAAS,qBAAkB,iBAAiB,GAAG;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,WAAmD;AAChE,UAAM,QAAQ,MAAM,KAAK,aAAa;AACtC,UAAM,UAAU,KAAK,UAAU,KAAK;AAEpC,UAAM,EAAE,OAAO,YAAY,QAAQ,IAAI;AACvC,UAAM,aAAa,YAAY,SAAY,MAAM,gBAAgB,OAAO,KAAK,CAAC;AAC9E,UAAM,UAAU,WAAW,aAAa,YAAY,cAAc,MAAM,eAAe,GAAG;AAE1F,UAAM,MAAM,GAAG,YAAY,aAAa,KAAK,aAAa,UAAU,UAAU,OAAO;AAErF,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,SAAS,sCAAmC,eAAe,SAAS,MAAM;AAAA,IACtF;AAEA,UAAM,OAAQ,MAAM,SAAS,KAAK;AAElC,QAAI,KAAK,WAAW,KAAK,CAAC,KAAK,QAAQ,KAAK,KAAK,WAAW,GAAG;AAC7D,YAAM,IAAI,SAAS,0CAAoC,kBAAkB,GAAG;AAAA,IAC9E;AAEA,WAAO,KAAK,KAAK,CAAC;AAAA,EACpB;AACF;AAEO,MAAM,cAAc,IAAI,YAAY;","names":[]}
@@ -1,6 +1,9 @@
1
1
  import { TEST_IDS } from "../constants";
2
2
  function b64(value) {
3
- return btoa(JSON.stringify(value));
3
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
4
+ let binary = "";
5
+ for (const byte of bytes) binary += String.fromCharCode(byte);
6
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4
7
  }
5
8
  function createTestJwt(claims = {}) {
6
9
  const payload = {
@@ -9,7 +12,7 @@ function createTestJwt(claims = {}) {
9
12
  continent: "SA",
10
13
  country: claims.country ?? "BR",
11
14
  region: "SP",
12
- city: "Sao Paulo",
15
+ city: "Palho\xE7a",
13
16
  latitude: "0",
14
17
  longitude: "0"
15
18
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/testing/factories/jwt.ts"],"sourcesContent":["import type { JWTPayload } from '../../modules/users/schema';\nimport { TEST_IDS } from '../constants';\n\nfunction b64(value: unknown): string {\n // getUserContext decodes the payload with `atob(token.split('.')[1])`, so standard base64 works.\n return btoa(JSON.stringify(value));\n}\n\nexport interface TestJwtClaims {\n id_wl?: number;\n id_account?: number;\n id_user?: number;\n // Country used by resolveLocale (session.location.country).\n country?: string;\n}\n\n// Builds a fake JWT (placeholder signature) whose payload is decodable by getUserContext and\n// resolveLocale. Used as DUMMY_AUTH_TOKEN: the common auth bypasses verification when that env is\n// set, so only the payload matters.\nexport function createTestJwt(claims: TestJwtClaims = {}): string {\n const payload: JWTPayload = {\n session: {\n location: {\n continent: 'SA',\n country: claims.country ?? 'BR',\n region: 'SP',\n city: 'Sao Paulo',\n latitude: '0',\n longitude: '0',\n },\n ip: '127.0.0.1',\n timezone: 'America/Sao_Paulo',\n agent: 'test',\n code: null,\n },\n id_wl: String(claims.id_wl ?? TEST_IDS.wl),\n id_account: claims.id_account ?? TEST_IDS.account,\n id_user: claims.id_user ?? TEST_IDS.user,\n iss: 'test',\n aud: 'test',\n iat: 1700000000,\n };\n return `${b64({ alg: 'none', typ: 'JWT' })}.${b64(payload)}.sig`;\n}\n"],"mappings":"AACA,SAAS,gBAAgB;AAEzB,SAAS,IAAI,OAAwB;AAEnC,SAAO,KAAK,KAAK,UAAU,KAAK,CAAC;AACnC;AAaO,SAAS,cAAc,SAAwB,CAAC,GAAW;AAChE,QAAM,UAAsB;AAAA,IAC1B,SAAS;AAAA,MACP,UAAU;AAAA,QACR,WAAW;AAAA,QACX,SAAS,OAAO,WAAW;AAAA,QAC3B,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,UAAU;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA,IAAI;AAAA,MACJ,UAAU;AAAA,MACV,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,IACA,OAAO,OAAO,OAAO,SAAS,SAAS,EAAE;AAAA,IACzC,YAAY,OAAO,cAAc,SAAS;AAAA,IAC1C,SAAS,OAAO,WAAW,SAAS;AAAA,IACpC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACA,SAAO,GAAG,IAAI,EAAE,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC;AAC5D;","names":[]}
1
+ {"version":3,"sources":["../../../src/testing/factories/jwt.ts"],"sourcesContent":["import type { JWTPayload } from '../../modules/users/schema';\nimport { TEST_IDS } from '../constants';\n\nfunction b64(value: unknown): string {\n // Produção emite o payload em base64url + UTF-8 (`@greatapps/jwt`), então a fixture faz igual.\n // Com `btoa` puro a fixture não conseguia sequer representar um payload acentuado — foi por isso\n // que o `atob` estrito dos decoders passou batido nos testes por tanto tempo.\n const bytes = new TextEncoder().encode(JSON.stringify(value));\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport interface TestJwtClaims {\n id_wl?: number;\n id_account?: number;\n id_user?: number;\n // Country used by resolveLocale (session.location.country).\n country?: string;\n}\n\n// Builds a fake JWT (placeholder signature) whose payload is decodable by getUserContext and\n// resolveLocale. Used as DUMMY_AUTH_TOKEN: the common auth bypasses verification when that env is\n// set, so only the payload matters.\nexport function createTestJwt(claims: TestJwtClaims = {}): string {\n const payload: JWTPayload = {\n session: {\n location: {\n continent: 'SA',\n country: claims.country ?? 'BR',\n region: 'SP',\n city: 'Palhoça',\n latitude: '0',\n longitude: '0',\n },\n ip: '127.0.0.1',\n timezone: 'America/Sao_Paulo',\n agent: 'test',\n code: null,\n },\n id_wl: String(claims.id_wl ?? TEST_IDS.wl),\n id_account: claims.id_account ?? TEST_IDS.account,\n id_user: claims.id_user ?? TEST_IDS.user,\n iss: 'test',\n aud: 'test',\n iat: 1700000000,\n };\n return `${b64({ alg: 'none', typ: 'JWT' })}.${b64(payload)}.sig`;\n}\n"],"mappings":"AACA,SAAS,gBAAgB;AAEzB,SAAS,IAAI,OAAwB;AAInC,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,KAAK,CAAC;AAC5D,MAAI,SAAS;AACb,aAAW,QAAQ,MAAO,WAAU,OAAO,aAAa,IAAI;AAC5D,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAaO,SAAS,cAAc,SAAwB,CAAC,GAAW;AAChE,QAAM,UAAsB;AAAA,IAC1B,SAAS;AAAA,MACP,UAAU;AAAA,QACR,WAAW;AAAA,QACX,SAAS,OAAO,WAAW;AAAA,QAC3B,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,UAAU;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA,IAAI;AAAA,MACJ,UAAU;AAAA,MACV,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,IACA,OAAO,OAAO,OAAO,SAAS,SAAS,EAAE;AAAA,IACzC,YAAY,OAAO,cAAc,SAAS;AAAA,IAC1C,SAAS,OAAO,WAAW,SAAS;AAAA,IACpC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACA,SAAO,GAAG,IAAI,EAAE,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC;AAC5D;","names":[]}
@@ -0,0 +1,15 @@
1
+ function decodeJwtPayload(token) {
2
+ const segment = token.split(".")[1];
3
+ if (!segment) {
4
+ throw new Error("JWT sem segmento de payload");
5
+ }
6
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
7
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
8
+ const binary = atob(padded);
9
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
10
+ return JSON.parse(new TextDecoder().decode(bytes));
11
+ }
12
+ export {
13
+ decodeJwtPayload
14
+ };
15
+ //# sourceMappingURL=jwt-payload.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/jwt-payload.ts"],"sourcesContent":["/**\n * Decodifica o payload de um JWT (o 2º segmento), SEM validar assinatura.\n *\n * Por que não dá pra usar `atob` direto no segmento:\n *\n * 1. **Alfabeto.** Quem emite os tokens é o `@greatapps/jwt`, que codifica com\n * `Buffer.from(...).toString('base64url')` — alfabeto com `-` e `_` e sem o padding `=`.\n * O `atob` é base64 ESTRITO: encontra `-`/`_` e lança `InvalidCharacterError`. Como os\n * dois caracteres só aparecem em certos alinhamentos de byte, o token de um usuário\n * quebra e o do vizinho não — o payload carrega geo-IP, IP e user-agent, então é o\n * conteúdo de cada sessão que decide (um usuário em \"Palhoça\" quebrava; em \"Curitiba\", não).\n *\n * 2. **Encoding.** `atob` devolve uma string latin1. Sem decodificar como UTF-8, todo\n * caractere não-ASCII do payload vira mojibake (`Palhoça` → `Palhoça`) mesmo quando\n * o decode não estoura.\n *\n * Lança se não houver payload ou se ele não for JSON válido — quem chama decide o que fazer.\n */\nexport function decodeJwtPayload<T>(token: string): T {\n const segment = token.split('.')[1];\n\n if (!segment) {\n throw new Error('JWT sem segmento de payload');\n }\n\n const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');\n const binary = atob(padded);\n const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));\n\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n"],"mappings":"AAkBO,SAAS,iBAAoB,OAAkB;AACpD,QAAM,UAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AAElC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAEA,QAAM,SAAS,QAAQ,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AAC3D,QAAM,SAAS,OAAO,OAAO,KAAK,KAAK,OAAO,SAAS,CAAC,IAAI,GAAG,GAAG;AAClE,QAAM,SAAS,KAAK,MAAM;AAC1B,QAAM,QAAQ,WAAW,KAAK,QAAQ,CAAC,cAAc,UAAU,WAAW,CAAC,CAAC;AAE5E,SAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;AACnD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.736",
3
+ "version": "1.1.738",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -4,6 +4,7 @@ import { cookies, headers } from 'next/headers';
4
4
  import { DEFAULT_LOCALE, Locale, LOCALE_COOKIE } from './config';
5
5
  import { localeFromCountry } from './country-language';
6
6
  import { normalizeLocale } from './normalize';
7
+ import { decodeJwtPayload } from '../utils/jwt-payload';
7
8
 
8
9
  const AUTH_COOKIE = 'greatapps';
9
10
  const GEO_HEADER = 'cf-ipcountry';
@@ -18,7 +19,7 @@ interface JwtLocaleInfo {
18
19
  function decodeJwtLocaleInfo(token?: string | null): JwtLocaleInfo | null {
19
20
  if (!token) return null;
20
21
  try {
21
- const payload = JSON.parse(atob(token.split('.')[1]));
22
+ const payload = decodeJwtPayload<{ id_wl?: number | string; session?: { location?: { country?: string | null } } }>(token);
22
23
  return {
23
24
  id_wl: payload?.id_wl != null ? Number(payload.id_wl) : undefined,
24
25
  country: payload?.session?.location?.country ?? null,
@@ -2,6 +2,7 @@ import 'server-only';
2
2
 
3
3
  import type { RequestConfig } from '../../../infra/api/client';
4
4
  import type { JWTPayload } from '../../users/schema';
5
+ import { decodeJwtPayload } from '../../../utils/jwt-payload';
5
6
  import { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';
6
7
  import { whitelabelService } from '../../whitelabel/services/whitelabel.service';
7
8
 
@@ -35,7 +36,7 @@ export async function buildWlOverrideFromJwt(
35
36
  cookie: string,
36
37
  ): Promise<RequestConfig | undefined> {
37
38
  try {
38
- const payload = JSON.parse(atob(cookie.split('.')[1])) as JWTPayload;
39
+ const payload = decodeJwtPayload<JWTPayload>(cookie);
39
40
  return buildWlOverride(Number(payload.id_wl));
40
41
  } catch {
41
42
  return undefined;
@@ -3,6 +3,7 @@ import 'server-only';
3
3
  import { cookies } from 'next/headers';
4
4
  import { ApiError } from '../../../infra/api/types';
5
5
  import { JWTPayload } from '../../users/schema';
6
+ import { decodeJwtPayload } from '../../../utils/jwt-payload';
6
7
 
7
8
  const AUTH_COOKIE_NAME = 'greatapps';
8
9
 
@@ -19,7 +20,7 @@ export async function getUserContext(): Promise<{
19
20
  }
20
21
 
21
22
  try {
22
- const payload = JSON.parse(atob(token.split('.')[1])) as JWTPayload;
23
+ const payload = decodeJwtPayload<JWTPayload>(token);
23
24
 
24
25
  return {
25
26
  id_wl: Number(payload.id_wl),
@@ -3,6 +3,7 @@ import 'server-only';
3
3
  import { cookies } from 'next/headers';
4
4
  import { decode } from '@greatapps/jwt';
5
5
  import { ApiError } from '../../../infra/api/types';
6
+ import { decodeJwtPayload } from '../../../utils/jwt-payload';
6
7
 
7
8
  const AUTH_COOKIE_NAME = 'greatapps';
8
9
 
@@ -33,7 +34,7 @@ export async function requireTokenNotExpired(): Promise<void> {
33
34
  }
34
35
 
35
36
  try {
36
- const payload = JSON.parse(atob(token.split('.')[1]));
37
+ const payload = decodeJwtPayload<{ id_wl?: number | string }>(token);
37
38
  const idWl = payload.id_wl;
38
39
 
39
40
  if (!idWl) {
@@ -27,6 +27,28 @@ import {
27
27
  type SubscriptionPendingPixResponse,
28
28
  } from "../types/pix-pending.type";
29
29
 
30
+ /**
31
+ * Timeout das mutations de assinatura que ESCREVEM (action/plan e action/payment).
32
+ *
33
+ * O default do ApiClient é 30s. Essas duas rotas caem no mesmo motor pesado do
34
+ * backend (planEdit delega pra paymentEdit) — troca de método de pagamento, mandato
35
+ * PIX recorrente Woovi, void-and-replace de fatura — e passam de 30s com alguma
36
+ * frequência: em produção elas aparecem como `outcome=canceled` com wallTime ~29.9s.
37
+ *
38
+ * O problema não é o usuário ver "Tempo excedido": é que o abort MATA o worker no
39
+ * meio da saga. Se o corte cai depois de criar o mandato Woovi e suprimir a fatura
40
+ * de ciclo, sobra mandato órfão + fatura sem dunning, o usuário não recebe QR nenhum
41
+ * e a próxima tentativa cria um segundo mandato. Deixar a request terminar converte
42
+ * uma escrita parcial numa escrita completa.
43
+ *
44
+ * Não é a solução definitiva — a rota deveria ser assíncrona (202 + polling no
45
+ * correlation_id). É o teto que dá folga enquanto isso.
46
+ *
47
+ * Só nas mutations: calculateSubscription é idempotente (não escreve nada), então
48
+ * um timeout nela não deixa rastro e não precisa esperar mais.
49
+ */
50
+ const SUBSCRIPTION_WRITE_TIMEOUT_MS = 60000;
51
+
30
52
  export type UpdateSubscriptionPlanResponse =
31
53
  | Subscription
32
54
  | SubscriptionPendingPixResponse;
@@ -106,6 +128,7 @@ class SubscriptionsService {
106
128
  >(
107
129
  `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,
108
130
  data,
131
+ { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },
109
132
  );
110
133
 
111
134
  if (response.status === 0) {
@@ -145,6 +168,7 @@ class SubscriptionsService {
145
168
  const response = await api.apps.put<ApiActionResult<void>>(
146
169
  `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,
147
170
  data,
171
+ { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },
148
172
  );
149
173
 
150
174
  if (response.status === 0) {
@@ -2,6 +2,7 @@
2
2
  import { cookies } from 'next/headers';
3
3
 
4
4
  import { ApiError } from '../../../infra/api/types';
5
+ import { decodeJwtPayload } from '../../../utils/jwt-payload';
5
6
  import { GetUserDataResponse, JWTPayload, User } from '../schema';
6
7
  import { findWhitelabel } from '../../whitelabel/actions/find-whitelabel.action';
7
8
  import { buildWlOverride } from '../../auth/utils/build-wl-override';
@@ -24,7 +25,7 @@ class UserService {
24
25
 
25
26
  private decodeJWT(token: string): JWTPayload {
26
27
  try {
27
- return JSON.parse(atob(token.split('.')[1])) as JWTPayload;
28
+ return decodeJwtPayload<JWTPayload>(token);
28
29
  } catch {
29
30
  throw new ApiError('Token inválido', 'INVALID_TOKEN', 401);
30
31
  }
@@ -2,8 +2,13 @@ import type { JWTPayload } from '../../modules/users/schema';
2
2
  import { TEST_IDS } from '../constants';
3
3
 
4
4
  function b64(value: unknown): string {
5
- // getUserContext decodes the payload with `atob(token.split('.')[1])`, so standard base64 works.
6
- return btoa(JSON.stringify(value));
5
+ // Produção emite o payload em base64url + UTF-8 (`@greatapps/jwt`), então a fixture faz igual.
6
+ // Com `btoa` puro a fixture não conseguia sequer representar um payload acentuado — foi por isso
7
+ // que o `atob` estrito dos decoders passou batido nos testes por tanto tempo.
8
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
9
+ let binary = '';
10
+ for (const byte of bytes) binary += String.fromCharCode(byte);
11
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
7
12
  }
8
13
 
9
14
  export interface TestJwtClaims {
@@ -24,7 +29,7 @@ export function createTestJwt(claims: TestJwtClaims = {}): string {
24
29
  continent: 'SA',
25
30
  country: claims.country ?? 'BR',
26
31
  region: 'SP',
27
- city: 'Sao Paulo',
32
+ city: 'Palhoça',
28
33
  latitude: '0',
29
34
  longitude: '0',
30
35
  },
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Decodifica o payload de um JWT (o 2º segmento), SEM validar assinatura.
3
+ *
4
+ * Por que não dá pra usar `atob` direto no segmento:
5
+ *
6
+ * 1. **Alfabeto.** Quem emite os tokens é o `@greatapps/jwt`, que codifica com
7
+ * `Buffer.from(...).toString('base64url')` — alfabeto com `-` e `_` e sem o padding `=`.
8
+ * O `atob` é base64 ESTRITO: encontra `-`/`_` e lança `InvalidCharacterError`. Como os
9
+ * dois caracteres só aparecem em certos alinhamentos de byte, o token de um usuário
10
+ * quebra e o do vizinho não — o payload carrega geo-IP, IP e user-agent, então é o
11
+ * conteúdo de cada sessão que decide (um usuário em "Palhoça" quebrava; em "Curitiba", não).
12
+ *
13
+ * 2. **Encoding.** `atob` devolve uma string latin1. Sem decodificar como UTF-8, todo
14
+ * caractere não-ASCII do payload vira mojibake (`Palhoça` → `Palhoça`) mesmo quando
15
+ * o decode não estoura.
16
+ *
17
+ * Lança se não houver payload ou se ele não for JSON válido — quem chama decide o que fazer.
18
+ */
19
+ export function decodeJwtPayload<T>(token: string): T {
20
+ const segment = token.split('.')[1];
21
+
22
+ if (!segment) {
23
+ throw new Error('JWT sem segmento de payload');
24
+ }
25
+
26
+ const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');
27
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
28
+ const binary = atob(padded);
29
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
30
+
31
+ return JSON.parse(new TextDecoder().decode(bytes)) as T;
32
+ }