@seedgrid/fe-security 2026.7.0 → 2026.7.2

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.
Files changed (125) hide show
  1. package/README.md +113 -0
  2. package/dist/client/index.d.ts +4 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +5 -0
  5. package/dist/client/permission-provider.d.ts +18 -0
  6. package/dist/client/permission-provider.d.ts.map +1 -0
  7. package/dist/client/permission-provider.js +57 -0
  8. package/dist/client/pick-list-field.d.ts +15 -0
  9. package/dist/client/pick-list-field.d.ts.map +1 -0
  10. package/dist/client/pick-list-field.js +30 -0
  11. package/dist/client/status-badge.d.ts +6 -0
  12. package/dist/client/status-badge.d.ts.map +1 -0
  13. package/dist/client/status-badge.js +6 -0
  14. package/dist/config.d.ts +34 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +4 -0
  17. package/dist/dtos.d.ts +88 -0
  18. package/dist/dtos.d.ts.map +1 -0
  19. package/dist/dtos.js +3 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +10 -0
  23. package/dist/menu.d.ts +19 -0
  24. package/dist/menu.d.ts.map +1 -0
  25. package/dist/menu.js +41 -0
  26. package/dist/messages.d.ts +1858 -0
  27. package/dist/messages.d.ts.map +1 -0
  28. package/dist/messages.js +990 -0
  29. package/dist/module.d.ts +3 -0
  30. package/dist/module.d.ts.map +1 -0
  31. package/dist/module.js +37 -0
  32. package/dist/module.json +21 -0
  33. package/dist/paths.d.ts +1 -0
  34. package/dist/paths.d.ts.map +1 -1
  35. package/dist/paths.js +3 -0
  36. package/dist/permissions.d.ts +14 -0
  37. package/dist/permissions.d.ts.map +1 -0
  38. package/dist/permissions.js +39 -0
  39. package/dist/server/action.d.ts +3 -0
  40. package/dist/server/action.d.ts.map +1 -0
  41. package/dist/server/action.js +21 -0
  42. package/dist/server/create-security-server.d.ts +4 -0
  43. package/dist/server/create-security-server.d.ts.map +1 -0
  44. package/dist/server/create-security-server.js +166 -0
  45. package/dist/server/errors.d.ts +13 -0
  46. package/dist/server/errors.d.ts.map +1 -0
  47. package/dist/server/errors.js +26 -0
  48. package/dist/server/index.d.ts +8 -0
  49. package/dist/server/index.d.ts.map +1 -0
  50. package/dist/server/index.js +8 -0
  51. package/dist/server/problem-response.d.ts +3 -0
  52. package/dist/server/problem-response.d.ts.map +1 -0
  53. package/dist/server/problem-response.js +28 -0
  54. package/dist/server/route.d.ts +3 -0
  55. package/dist/server/route.d.ts.map +1 -0
  56. package/dist/server/route.js +22 -0
  57. package/dist/server/to-problem.d.ts +3 -0
  58. package/dist/server/to-problem.d.ts.map +1 -0
  59. package/dist/server/to-problem.js +22 -0
  60. package/dist/server-contract.d.ts +23 -0
  61. package/dist/server-contract.d.ts.map +1 -0
  62. package/dist/server-contract.js +4 -0
  63. package/dist/session-contract.d.ts +36 -0
  64. package/dist/session-contract.d.ts.map +1 -0
  65. package/dist/session-contract.js +8 -0
  66. package/dist/session-result.d.ts +22 -0
  67. package/dist/session-result.d.ts.map +1 -0
  68. package/dist/session-result.js +14 -0
  69. package/package.json +38 -3
  70. package/scaffold/README.md +97 -0
  71. package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
  72. package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
  73. package/scaffold/src/app/api/security/session/login/route.ts +7 -0
  74. package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
  75. package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
  76. package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
  77. package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
  78. package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
  79. package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
  80. package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
  81. package/scaffold/src/app/auth/login/page.tsx +386 -0
  82. package/scaffold/src/app/auth/welcome/page.tsx +90 -0
  83. package/scaffold/src/app/login/page.tsx +7 -0
  84. package/scaffold/src/app/page.tsx +12 -0
  85. package/scaffold/src/app/security/2fa/page.tsx +370 -0
  86. package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
  87. package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
  88. package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
  89. package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
  90. package/scaffold/src/app/security/profile/page.tsx +492 -0
  91. package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
  92. package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
  93. package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
  94. package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
  95. package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
  96. package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
  97. package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
  98. package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
  99. package/scaffold/src/app/users/change-password/page.tsx +172 -0
  100. package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
  101. package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
  102. package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
  103. package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
  104. package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
  105. package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
  106. package/scaffold/src/modules/security/config.ts +55 -0
  107. package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
  108. package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
  109. package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
  110. package/scaffold/src/modules/security/host-access.ts +59 -0
  111. package/scaffold/src/modules/security/index.ts +44 -0
  112. package/scaffold/src/modules/security/navigation.ts +41 -0
  113. package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
  114. package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
  115. package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
  116. package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
  117. package/scaffold/src/modules/security/services/auth-service.ts +332 -0
  118. package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
  119. package/scaffold/src/modules/security/services/security-api.ts +281 -0
  120. package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
  121. package/scaffold/src/modules/security/services/security-service.ts +727 -0
  122. package/scaffold/src/modules/security/services/session-storage.ts +80 -0
  123. package/scaffold/src/modules/security/types.ts +295 -0
  124. package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
  125. package/scaffold/src/modules/security/utils.ts +194 -0
@@ -0,0 +1,3 @@
1
+ import type { SeedGridCliModuleManifest } from "@seedgrid/fe-core";
2
+ export declare const module: SeedGridCliModuleManifest;
3
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AA+BnE,eAAO,MAAM,MAAM,EAAE,yBAOpB,CAAC"}
package/dist/module.js ADDED
@@ -0,0 +1,37 @@
1
+ // Reference implementation of the manifest read by the SeedGrid CLI (Rust).
2
+ // See seedgrid-cli/docs/adr/0004-module-manifest-contract.md.
3
+ //
4
+ // `provider.from` is a scaffold-relative app alias (`@/modules/security`),
5
+ // not an npm import: `AuthProvider` runs the full login/session/redirect
6
+ // flow and only exists in scaffold (it hardcodes scaffold routes — see
7
+ // scaffold/README.md for the lib-vs-scaffold split found while extracting
8
+ // this module). The wiring mechanism is plain string substitution into a
9
+ // marker, so it doesn't care whether `from` resolves to an npm package or an
10
+ // app-local alias — this field just needs to be *some* valid import
11
+ // specifier for the generated app.
12
+ //
13
+ // `@seedgrid/fe-security/client` also exports `PermissionProvider`, a
14
+ // lighter permission-only context (ported from admin-web's
15
+ // `AdminAccessProvider`) for apps that already have their own login/session
16
+ // flow and only want permission checks. That's a different consumption
17
+ // pattern from CLI-scaffolded apps, so it is not referenced here.
18
+ //
19
+ // `navigation` uses the scaffold-local hook via the `@/modules/security` alias
20
+ // — same mechanism as `provider` above (plain string substitution into the
21
+ // app-shell-navigation markers, so a scaffold alias is fine even though the
22
+ // hook hardcodes scaffold routes). Without this the CLI never wires
23
+ // `...useSecurityAppShellSections()` into the app shell and the generated app
24
+ // ships with an EMPTY navigation menu.
25
+ //
26
+ // `env` lists only what is verified today (the service-token fallback read
27
+ // directly from `admin-web/lib/seedgrid-report-api-server.ts`). The base API
28
+ // URL is resolved per-tenant from the hostname, not a static env var, so it
29
+ // is not listed here.
30
+ export const module = {
31
+ id: "security",
32
+ configKey: "security",
33
+ env: ["SEEDGRID_REPORT_API_TOKEN"],
34
+ provider: [{ import: "AuthProvider", from: "@/modules/security" }],
35
+ navigation: { import: "useSecurityAppShellSections", from: "@/modules/security" },
36
+ messages: { import: "securityMessages", from: "@seedgrid/fe-security" },
37
+ };
@@ -0,0 +1,21 @@
1
+ {
2
+ "id": "security",
3
+ "configKey": "security",
4
+ "env": [
5
+ "SEEDGRID_REPORT_API_TOKEN"
6
+ ],
7
+ "provider": [
8
+ {
9
+ "import": "AuthProvider",
10
+ "from": "@/modules/security"
11
+ }
12
+ ],
13
+ "navigation": {
14
+ "import": "useSecurityAppShellSections",
15
+ "from": "@/modules/security"
16
+ },
17
+ "messages": {
18
+ "import": "securityMessages",
19
+ "from": "@seedgrid/fe-security"
20
+ }
21
+ }
package/dist/paths.d.ts CHANGED
@@ -40,6 +40,7 @@ export declare const SecurityPaths: {
40
40
  /** ME (/me) — escopo do usuário corrente pelo token. */
41
41
  readonly me: {
42
42
  readonly permissions: "/me/permissions";
43
+ readonly companies: "/me/companies";
43
44
  readonly selectCompany: "/me/select-company";
44
45
  };
45
46
  /** COMPANIES (/companies, /public/companies). */
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,aAAa;IACxB,6CAA6C;;;;;;;;IAS7C,gEAAgE;;;;;;;;;;IAWhE,sBAAsB;;;;;wCAKG,MAAM;oCACV,MAAM;wCAEF,MAAM,mBAAmB,MAAM;;IAIxD,sBAAsB;;;;wCAIG,MAAM;;IAG/B,kCAAkC;;;wCAGT,MAAM;;IAG/B,wDAAwD;;;;;IAMxD,iDAAiD;;;;wCAIxB,MAAM;;IAG/B,gFAAgF;;;;;mCAK5D,MAAM;oCACL,MAAM;mCACP,MAAM;+BACV,MAAM;8BACP,MAAM;4CACQ,MAAM;6CACL,MAAM;wCAEX,MAAM;;CAEvB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,aAAa;IACxB,6CAA6C;;;;;;;;IAS7C,gEAAgE;;;;;;;;;;IAWhE,sBAAsB;;;;;wCAKG,MAAM;oCACV,MAAM;wCAEF,MAAM,mBAAmB,MAAM;;IAIxD,sBAAsB;;;;wCAIG,MAAM;;IAG/B,kCAAkC;;;wCAGT,MAAM;;IAG/B,wDAAwD;;;;;;IASxD,iDAAiD;;;;wCAIxB,MAAM;;IAG/B,gFAAgF;;;;;mCAK5D,MAAM;oCACL,MAAM;mCACP,MAAM;+BACV,MAAM;8BACP,MAAM;4CACQ,MAAM;6CACL,MAAM;wCAEX,MAAM;;CAEvB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC"}
package/dist/paths.js CHANGED
@@ -48,6 +48,9 @@ export const SecurityPaths = {
48
48
  /** ME (/me) — escopo do usuário corrente pelo token. */
49
49
  me: {
50
50
  permissions: "/me/permissions",
51
+ // Empresas (memberships) do usuário, para o seletor de empresa (login-tenancy).
52
+ companies: "/me/companies",
53
+ // Troca a empresa corrente e re-emite o par de tokens escopado (login-tenancy).
51
54
  selectCompany: "/me/select-company",
52
55
  },
53
56
  /** COMPANIES (/companies, /public/companies). */
@@ -0,0 +1,14 @@
1
+ type PermissionInput = Iterable<string> | Set<string>;
2
+ export declare function normalizePermission(permission: string | null | undefined): string;
3
+ export declare function createPermissionSet(permissions: Iterable<string>): Set<string>;
4
+ export declare function hasPermission(permissions: PermissionInput, permission: string | null | undefined): boolean;
5
+ export declare function hasAnyPermission(permissions: PermissionInput, requiredPermissions: Array<string | null | undefined>): boolean;
6
+ export declare function hasAllPermissions(permissions: PermissionInput, requiredPermissions: Array<string | null | undefined>): boolean;
7
+ export declare function buildCrudPermissions(resource: string): {
8
+ read: string;
9
+ create: string;
10
+ update: string;
11
+ delete: string;
12
+ };
13
+ export {};
14
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,KAAK,eAAe,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;AAEtD,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,UAMxE;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,eAMhE;AAQD,wBAAgB,aAAa,CAC3B,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,WAStC;AAED,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,eAAe,EAC5B,mBAAmB,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,WAKtD;AAED,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,eAAe,EAC5B,mBAAmB,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,WAKtD;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM;;;;;EAYpD"}
@@ -0,0 +1,39 @@
1
+ export function normalizePermission(permission) {
2
+ return String(permission ?? "")
3
+ .trim()
4
+ .replace(/[.\s-]+/g, "_")
5
+ .replace(/__+/g, "_")
6
+ .toUpperCase();
7
+ }
8
+ export function createPermissionSet(permissions) {
9
+ return new Set(Array.from(permissions)
10
+ .map((permission) => normalizePermission(permission))
11
+ .filter(Boolean));
12
+ }
13
+ function ensurePermissionSet(permissions) {
14
+ return permissions instanceof Set
15
+ ? createPermissionSet(permissions)
16
+ : createPermissionSet(permissions);
17
+ }
18
+ export function hasPermission(permissions, permission) {
19
+ const normalizedPermission = normalizePermission(permission);
20
+ if (!normalizedPermission) {
21
+ return false;
22
+ }
23
+ return ensurePermissionSet(permissions).has(normalizedPermission);
24
+ }
25
+ export function hasAnyPermission(permissions, requiredPermissions) {
26
+ return requiredPermissions.some((permission) => hasPermission(permissions, permission));
27
+ }
28
+ export function hasAllPermissions(permissions, requiredPermissions) {
29
+ return requiredPermissions.every((permission) => hasPermission(permissions, permission));
30
+ }
31
+ export function buildCrudPermissions(resource) {
32
+ const normalizedResource = normalizePermission(resource).replace(/_(READ|CREATE|UPDATE|DELETE)$/, "");
33
+ return {
34
+ read: `${normalizedResource}_READ`,
35
+ create: `${normalizedResource}_CREATE`,
36
+ update: `${normalizedResource}_UPDATE`,
37
+ delete: `${normalizedResource}_DELETE`,
38
+ };
39
+ }
@@ -0,0 +1,3 @@
1
+ import type { ActionResult } from "../session-result";
2
+ export declare function toActionResult<T>(run: () => Promise<T>): Promise<ActionResult<T>>;
3
+ //# sourceMappingURL=action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../src/server/action.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD,wBAAsB,cAAc,CAAC,CAAC,EACpC,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAM1B"}
@@ -0,0 +1,21 @@
1
+ // Helper de Server Action (mutações). Roda o motor de sessão e devolve um ActionResult
2
+ // tipado — NUNCA lança pro client (evita a redação de erro do Next em produção).
3
+ //
4
+ // Uso na action:
5
+ // "use server";
6
+ // export async function updateUser(input) {
7
+ // return toActionResult(() =>
8
+ // security.apiFetchSecurity(SecurityPaths.users.byPublicId(input.id), {
9
+ // method: "PUT", body: JSON.stringify(input),
10
+ // }),
11
+ // );
12
+ // }
13
+ import { toProblem } from "./to-problem";
14
+ export async function toActionResult(run) {
15
+ try {
16
+ return { ok: true, data: await run() };
17
+ }
18
+ catch (error) {
19
+ return { ok: false, problem: toProblem(error) };
20
+ }
21
+ }
@@ -0,0 +1,4 @@
1
+ import type { SecurityServerConfig } from "../config";
2
+ import type { SecurityServer } from "../server-contract";
3
+ export declare function createSecurityServer(config: SecurityServerConfig): SecurityServer;
4
+ //# sourceMappingURL=create-security-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-security-server.d.ts","sourceRoot":"","sources":["../../src/server/create-security-server.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAMtD,OAAO,KAAK,EAEV,cAAc,EACf,MAAM,oBAAoB,CAAC;AAE5B,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,oBAAoB,GAC3B,cAAc,CAsMhB"}
@@ -0,0 +1,166 @@
1
+ // Motor de sessão server-side do SeedGrid Security (portado do admin-web
2
+ // lib/seedgrid-report-api-server.ts, com os hardcodes parametrizados via
3
+ // SecurityServerConfig). SERVER-ONLY: usa next/headers.
4
+ //
5
+ // O app cria uma instância uma vez e importa os helpers ligados nos route handlers:
6
+ // export const security = createSecurityServer({ apiBaseUrl, cookies, ... });
7
+ import { cookies } from "next/headers";
8
+ import { ApiClientError } from "@seedgrid/fe-core";
9
+ import { SecurityPaths } from "../paths";
10
+ import { isLoginSuccess, } from "../session-contract";
11
+ import { ApiConnectionError, ApiRequestError } from "./errors";
12
+ export function createSecurityServer(config) {
13
+ const secure = config.secureCookies ?? process.env.NODE_ENV === "production";
14
+ async function buildHeaders(headers) {
15
+ const cookieStore = await cookies();
16
+ const sessionToken = cookieStore.get(config.cookies.token)?.value;
17
+ const tenancy = config.resolveTenancyHeaders
18
+ ? await config.resolveTenancyHeaders()
19
+ : undefined;
20
+ return {
21
+ ...(sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {}),
22
+ ...(!sessionToken && config.serviceToken
23
+ ? { Authorization: `Bearer ${config.serviceToken}` }
24
+ : {}),
25
+ ...tenancy,
26
+ ...headers,
27
+ };
28
+ }
29
+ async function safeFetch(path, options = {}) {
30
+ try {
31
+ return await fetch(`${config.apiBaseUrl}${path}`, {
32
+ ...options,
33
+ headers: await buildHeaders({
34
+ "Content-Type": "application/json",
35
+ ...options.headers,
36
+ }),
37
+ cache: "no-store",
38
+ });
39
+ }
40
+ catch (error) {
41
+ throw new ApiConnectionError(path, error);
42
+ }
43
+ }
44
+ async function applySessionCookies(data) {
45
+ const cookieStore = await cookies();
46
+ const opts = { httpOnly: true, path: "/", sameSite: "lax", secure };
47
+ cookieStore.set(config.cookies.token, data.token, opts);
48
+ if (typeof data.refreshToken === "string") {
49
+ cookieStore.set(config.cookies.refresh, data.refreshToken, opts);
50
+ }
51
+ if (data.changePasswordRequired) {
52
+ cookieStore.set(config.cookies.passwordChangeRequired, "1", opts);
53
+ }
54
+ else {
55
+ cookieStore.delete(config.cookies.passwordChangeRequired);
56
+ }
57
+ }
58
+ async function clearAuthCookies() {
59
+ const cookieStore = await cookies();
60
+ cookieStore.delete(config.cookies.token);
61
+ cookieStore.delete(config.cookies.refresh);
62
+ cookieStore.delete(config.cookies.passwordChangeRequired);
63
+ }
64
+ async function tryRefreshSession() {
65
+ const cookieStore = await cookies();
66
+ const refreshToken = cookieStore.get(config.cookies.refresh)?.value;
67
+ if (!refreshToken) {
68
+ await clearAuthCookies();
69
+ return false;
70
+ }
71
+ let response;
72
+ try {
73
+ response = await fetch(`${config.apiBaseUrl}${SecurityPaths.auth.refresh}`, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify({ refreshToken }),
77
+ cache: "no-store",
78
+ });
79
+ }
80
+ catch (error) {
81
+ await clearAuthCookies();
82
+ throw new ApiConnectionError(SecurityPaths.auth.refresh, error);
83
+ }
84
+ if (!response.ok) {
85
+ await clearAuthCookies();
86
+ return false;
87
+ }
88
+ const payload = (await response.json());
89
+ if (!isLoginSuccess(payload)) {
90
+ await clearAuthCookies();
91
+ return false;
92
+ }
93
+ await applySessionCookies(payload);
94
+ return true;
95
+ }
96
+ async function fetchWithSession(path, options = {}) {
97
+ let response = await safeFetch(path, options);
98
+ if (response.status !== 401) {
99
+ return response;
100
+ }
101
+ const refreshed = await tryRefreshSession();
102
+ if (!refreshed) {
103
+ return response;
104
+ }
105
+ response = await safeFetch(path, options);
106
+ return response;
107
+ }
108
+ function isInvalidSessionResponse(status, body) {
109
+ // Apenas 401 = sessão inválida/expirada (limpa cookies → login). 403 NÃO: é
110
+ // autenticado SEM permissão (sessão válida) — a tela só mostra "Acesso negado",
111
+ // nunca desloga. Tratar 403 como inválido fazia clicar num item proibido jogar o
112
+ // usuário pro /login.
113
+ if (status === 401) {
114
+ return true;
115
+ }
116
+ const normalized = body.toLocaleLowerCase("pt-BR");
117
+ return (normalized.includes("usuario nao encontrado") ||
118
+ normalized.includes("usuário não encontrado"));
119
+ }
120
+ function parseResponseBody(text) {
121
+ const trimmed = text.trim();
122
+ if (!trimmed) {
123
+ return null;
124
+ }
125
+ try {
126
+ return JSON.parse(trimmed);
127
+ }
128
+ catch {
129
+ return text;
130
+ }
131
+ }
132
+ async function apiFetchServer(path, options = {}) {
133
+ const response = await fetchWithSession(path, options);
134
+ if (!response.ok) {
135
+ const text = await response.text();
136
+ if (isInvalidSessionResponse(response.status, text)) {
137
+ await clearAuthCookies();
138
+ }
139
+ throw new ApiRequestError(response.status, text);
140
+ }
141
+ return (await response.json());
142
+ }
143
+ async function apiFetchSecurity(path, options = {}) {
144
+ const response = await fetchWithSession(path, options);
145
+ if (!response.ok) {
146
+ const text = await response.text();
147
+ if (isInvalidSessionResponse(response.status, text)) {
148
+ await clearAuthCookies();
149
+ }
150
+ throw new ApiClientError(`API request failed with status ${response.status}`, response.status, parseResponseBody(text));
151
+ }
152
+ if (response.status === 204) {
153
+ return null;
154
+ }
155
+ return (await response.json());
156
+ }
157
+ return {
158
+ fetchWithSession,
159
+ apiFetchSecurity,
160
+ apiFetchServer,
161
+ buildHeaders,
162
+ applySessionCookies,
163
+ clearAuthCookies,
164
+ isInvalidSessionResponse,
165
+ };
166
+ }
@@ -0,0 +1,13 @@
1
+ /** Erro de request com corpo cru (usado pelo apiFetchServer, retrocompat). */
2
+ export declare class ApiRequestError extends Error {
3
+ readonly status: number;
4
+ readonly body: string;
5
+ constructor(status: number, body: string);
6
+ }
7
+ /** Falha de conexão com o report-api (rede indisponível, DNS, etc.). */
8
+ export declare class ApiConnectionError extends Error {
9
+ readonly path: string;
10
+ constructor(path: string, cause?: unknown);
11
+ }
12
+ export declare function isApiConnectionError(error: unknown): error is ApiConnectionError;
13
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/server/errors.ts"],"names":[],"mappings":"AAGA,8EAA8E;AAC9E,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAMzC;AAED,wEAAwE;AACxE,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM1C;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,kBAAkB,CAEhF"}
@@ -0,0 +1,26 @@
1
+ // Erros do motor de sessão server-side. `ApiClientError` (repasse do problem+json)
2
+ // vem do @seedgrid/fe-core e é usado pelo apiFetchSecurity/problemResponse.
3
+ /** Erro de request com corpo cru (usado pelo apiFetchServer, retrocompat). */
4
+ export class ApiRequestError extends Error {
5
+ status;
6
+ body;
7
+ constructor(status, body) {
8
+ super(body || `API request failed: ${status}`);
9
+ this.name = "ApiRequestError";
10
+ this.status = status;
11
+ this.body = body;
12
+ }
13
+ }
14
+ /** Falha de conexão com o report-api (rede indisponível, DNS, etc.). */
15
+ export class ApiConnectionError extends Error {
16
+ path;
17
+ constructor(path, cause) {
18
+ super(`Não foi possível conectar à API em ${path}.`);
19
+ this.name = "ApiConnectionError";
20
+ this.path = path;
21
+ this.cause = cause;
22
+ }
23
+ }
24
+ export function isApiConnectionError(error) {
25
+ return error instanceof ApiConnectionError;
26
+ }
@@ -0,0 +1,8 @@
1
+ export { createSecurityServer } from "./create-security-server";
2
+ export type { SecurityServer, SecurityFetchOptions, } from "../server-contract";
3
+ export { ApiRequestError, ApiConnectionError, isApiConnectionError, } from "./errors";
4
+ export { problemResponse } from "./problem-response";
5
+ export { toProblem } from "./to-problem";
6
+ export { toActionResult } from "./action";
7
+ export { toRouteResponse } from "./route";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,YAAY,EACV,cAAc,EACd,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,8 @@
1
+ // Barrel SERVER-ONLY (@seedgrid/fe-security/server). Usa next/headers e next/server —
2
+ // NÃO importe daqui em componentes client.
3
+ export { createSecurityServer } from "./create-security-server";
4
+ export { ApiRequestError, ApiConnectionError, isApiConnectionError, } from "./errors";
5
+ export { problemResponse } from "./problem-response";
6
+ export { toProblem } from "./to-problem";
7
+ export { toActionResult } from "./action";
8
+ export { toRouteResponse } from "./route";
@@ -0,0 +1,3 @@
1
+ import { NextResponse } from "next/server";
2
+ export declare function problemResponse(error: unknown): NextResponse;
3
+ //# sourceMappingURL=problem-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"problem-response.d.ts","sourceRoot":"","sources":["../../src/server/problem-response.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAiC5D"}
@@ -0,0 +1,28 @@
1
+ // Converte o erro lançado por uma chamada server-side (ApiClientError) na resposta que
2
+ // o route handler do Next devolve ao client. Repassa o `responseBody` (problem+json
3
+ // original do report-api) com o `status` upstream, como application/problem+json —
4
+ // assim o `extractApiErrorMessage` do lado client recupera a mensagem íntegra.
5
+ import { NextResponse } from "next/server";
6
+ import { ApiClientError } from "@seedgrid/fe-core";
7
+ export function problemResponse(error) {
8
+ if (error instanceof ApiClientError) {
9
+ const { responseBody, status } = error;
10
+ if (typeof responseBody === "string") {
11
+ return new NextResponse(responseBody, {
12
+ status,
13
+ headers: { "Content-Type": "application/problem+json" },
14
+ });
15
+ }
16
+ if (responseBody != null) {
17
+ return NextResponse.json(responseBody, {
18
+ status,
19
+ headers: { "Content-Type": "application/problem+json" },
20
+ });
21
+ }
22
+ return NextResponse.json({ status, title: error.message }, { status, headers: { "Content-Type": "application/problem+json" } });
23
+ }
24
+ const detail = error instanceof Error
25
+ ? error.message
26
+ : "Não foi possível conectar à API.";
27
+ return NextResponse.json({ status: 502, title: "Bad Gateway", detail }, { status: 502, headers: { "Content-Type": "application/problem+json" } });
28
+ }
@@ -0,0 +1,3 @@
1
+ import { NextResponse } from "next/server";
2
+ export declare function toRouteResponse<T>(run: () => Promise<T>): Promise<NextResponse>;
3
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/server/route.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,wBAAsB,eAAe,CAAC,CAAC,EACrC,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,YAAY,CAAC,CAUvB"}
@@ -0,0 +1,22 @@
1
+ // Helper de route handler (leituras GET — cacheáveis). Roda o motor de sessão e
2
+ // devolve um NextResponse: sucesso → json; erro → application/problem+json (repassa o
3
+ // problem+json do report-api íntegro, pro extractApiErrorMessage do client ler).
4
+ //
5
+ // Uso no route handler:
6
+ // export async function GET() {
7
+ // return toRouteResponse(() => security.apiFetchSecurity(SecurityPaths.users.base));
8
+ // }
9
+ import { NextResponse } from "next/server";
10
+ import { problemResponse } from "./problem-response";
11
+ export async function toRouteResponse(run) {
12
+ try {
13
+ const data = await run();
14
+ if (data === null || data === undefined) {
15
+ return new NextResponse(null, { status: 204 });
16
+ }
17
+ return NextResponse.json(data);
18
+ }
19
+ catch (error) {
20
+ return problemResponse(error);
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProblemDetail } from "../session-result";
2
+ export declare function toProblem(error: unknown): ProblemDetail;
3
+ //# sourceMappingURL=to-problem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-problem.d.ts","sourceRoot":"","sources":["../../src/server/to-problem.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,CAoBvD"}
@@ -0,0 +1,22 @@
1
+ // Converte um erro server-side (ApiClientError do motor de sessão, ou falha de rede)
2
+ // num ProblemDetail serializável. Base compartilhada por route handler e Server Action.
3
+ import { ApiClientError } from "@seedgrid/fe-core";
4
+ export function toProblem(error) {
5
+ if (error instanceof ApiClientError) {
6
+ const body = error.responseBody;
7
+ // problem+json do report-api: repassa o corpo inteiro (detail/violations/title),
8
+ // preservando o status do body quando presente.
9
+ if (body && typeof body === "object") {
10
+ return { status: error.status, ...body };
11
+ }
12
+ if (typeof body === "string") {
13
+ return { status: error.status, detail: body };
14
+ }
15
+ return { status: error.status, title: error.message };
16
+ }
17
+ return {
18
+ status: 502,
19
+ title: "Bad Gateway",
20
+ detail: error instanceof Error ? error.message : "Erro de conexão.",
21
+ };
22
+ }
@@ -0,0 +1,23 @@
1
+ import type { SecurityLoginResponse } from "./session-contract";
2
+ export type SecurityFetchOptions = Omit<RequestInit, "headers"> & {
3
+ headers?: HeadersInit;
4
+ };
5
+ export interface SecurityServer {
6
+ /** Fetch cru contra o report-api com sessão (cookie→Bearer) e refresh em 401. */
7
+ fetchWithSession(path: string, options?: SecurityFetchOptions): Promise<Response>;
8
+ /** Canônico do fluxo de segurança: em erro lança ApiClientError (p/ problemResponse). */
9
+ apiFetchSecurity<T>(path: string, options?: SecurityFetchOptions): Promise<T>;
10
+ /** Variante retrocompat: em erro lança ApiRequestError (corpo cru). */
11
+ apiFetchServer<T>(path: string, options?: SecurityFetchOptions): Promise<T>;
12
+ /** Monta os headers (Authorization da sessão/service + tenancy) para uma request. */
13
+ buildHeaders(headers?: HeadersInit): Promise<HeadersInit>;
14
+ /** Grava os cookies de sessão a partir de um login/select-company OK. */
15
+ applySessionCookies(data: Extract<SecurityLoginResponse, {
16
+ status: "OK";
17
+ }>): Promise<void>;
18
+ /** Apaga os cookies de sessão. */
19
+ clearAuthCookies(): Promise<void>;
20
+ /** 401 = sessão inválida (desloga). 403 = autenticado sem permissão (NÃO desloga). */
21
+ isInvalidSessionResponse(status: number, body: string): boolean;
22
+ }
23
+ //# sourceMappingURL=server-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-contract.d.ts","sourceRoot":"","sources":["../src/server-contract.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhE,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG;IAChE,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,gBAAgB,CACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrB,yFAAyF;IACzF,gBAAgB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9E,uEAAuE;IACvE,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5E,qFAAqF;IACrF,YAAY,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1D,yEAAyE;IACzE,mBAAmB,CACjB,IAAI,EAAE,OAAO,CAAC,qBAAqB,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,kCAAkC;IAClC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,sFAAsF;IACtF,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACjE"}
@@ -0,0 +1,4 @@
1
+ // Contrato PURO do motor de sessão (client-safe — só tipos, sem next/*). Fica na raiz
2
+ // pra outros pacotes (ex.: fe-login-tenancy) importarem o tipo SecurityServer sem
3
+ // puxar a implementação server (next/headers). A implementação vive em ./server.
4
+ export {};
@@ -0,0 +1,36 @@
1
+ export type SecurityLoginResponse = {
2
+ status: "OK";
3
+ token: string;
4
+ tokenType?: string;
5
+ refreshToken?: string;
6
+ publicId?: string;
7
+ changePasswordRequired?: boolean;
8
+ twoFactorEnabled?: boolean;
9
+ } | {
10
+ status: "2F_REQUIRED";
11
+ tempToken: string;
12
+ method?: string;
13
+ } | {
14
+ status: string;
15
+ [key: string]: unknown;
16
+ };
17
+ export type SecurityResend2FAResponse = {
18
+ status: string;
19
+ method?: string;
20
+ message?: string;
21
+ tempToken?: string;
22
+ metadata?: Record<string, unknown>;
23
+ error?: string;
24
+ };
25
+ /** Escopo de permissões do usuário corrente (GET /me/permissions). */
26
+ export interface MePermissions {
27
+ userPublicId: string;
28
+ name: string;
29
+ roles: string[];
30
+ permissions: string[];
31
+ }
32
+ /** Narrowing: login concluído com token (status "OK"). */
33
+ export declare function isLoginSuccess(payload: SecurityLoginResponse): payload is Extract<SecurityLoginResponse, {
34
+ status: "OK";
35
+ }>;
36
+ //# sourceMappingURL=session-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-contract.d.ts","sourceRoot":"","sources":["../src/session-contract.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,qBAAqB,GAC7B;IACE,MAAM,EAAE,IAAI,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GACD;IACE,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACD;IACE,MAAM,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEN,MAAM,MAAM,yBAAyB,GAAG;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,GAC7B,OAAO,IAAI,OAAO,CAAC,qBAAqB,EAAE;IAAE,MAAM,EAAE,IAAI,CAAA;CAAE,CAAC,CAM7D"}
@@ -0,0 +1,8 @@
1
+ // Contrato de resposta do login do seedgrid-security (espelha o backend). É genérico
2
+ // aos dois modos de tenancy — por isso mora no barrel client-safe, não no /server.
3
+ /** Narrowing: login concluído com token (status "OK"). */
4
+ export function isLoginSuccess(payload) {
5
+ return (payload.status === "OK" &&
6
+ "token" in payload &&
7
+ typeof payload.token === "string");
8
+ }
@@ -0,0 +1,22 @@
1
+ /** Problem Details (RFC 7807) serializável — o corpo de erro do report-api. */
2
+ export interface ProblemDetail {
3
+ status: number;
4
+ title?: string;
5
+ detail?: string;
6
+ /** Violações de validação (se o backend enviar). */
7
+ violations?: Array<{
8
+ field?: string;
9
+ message?: string;
10
+ }>;
11
+ [key: string]: unknown;
12
+ }
13
+ export type ActionResult<T> = {
14
+ ok: true;
15
+ data: T;
16
+ } | {
17
+ ok: false;
18
+ problem: ProblemDetail;
19
+ };
20
+ /** Lê a mensagem exibível de um ProblemDetail (detail → 1ª violação → title → status). */
21
+ export declare function readProblemMessage(problem: ProblemDetail): string;
22
+ //# sourceMappingURL=session-result.d.ts.map