@dacyti-gsi/auth 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dacyti-gsi/auth",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Cliente del SSO de la división: OIDC (PKCE), sesión y permisos para Next.js/React",
5
5
  "keywords": [
6
6
  "sso",
@@ -65,7 +65,7 @@
65
65
  "@dacyti-gsi/config": "^0.1.0",
66
66
  "@types/node": "^20.19.43",
67
67
  "@types/react": "^19",
68
- "next": "16.4.0",
68
+ "next": "16.4.2",
69
69
  "react": "19.3.0",
70
70
  "typescript": "^5",
71
71
  "vitest": "^4.1.10"
package/src/config.ts CHANGED
@@ -78,7 +78,10 @@ function isLocalhost(url: URL): boolean {
78
78
  function isSecureOrLocal(value: string): boolean {
79
79
  try {
80
80
  const url = new URL(value);
81
- return url.protocol === "https:" || (url.protocol === "http:" && isLocalhost(url));
81
+ return (
82
+ url.protocol === "https:" ||
83
+ (url.protocol === "http:" && isLocalhost(url))
84
+ );
82
85
  } catch {
83
86
  return false;
84
87
  }
package/src/core.ts CHANGED
@@ -446,3 +446,56 @@ export function refreshOnce(
446
446
 
447
447
  return pending;
448
448
  }
449
+
450
+ /**
451
+ * Envía el manifiesto (permisos y roles) de la aplicación al portal.
452
+ *
453
+ * Autentica con `client_secret_basic`, las mismas credenciales con las que ya se
454
+ * canjean los códigos. El portal lo aplica de forma idempotente y solo sobre el
455
+ * catálogo de este `clientId`, así que se puede llamar en cada arranque.
456
+ */
457
+ export async function syncManifest(input: {
458
+ issuer: string;
459
+ clientId: string;
460
+ clientSecret: string;
461
+ permissions: readonly string[];
462
+ roles: Readonly<Record<string, readonly string[]>>;
463
+ }): Promise<{ permissions: number; roles: number }> {
464
+ const endpoint = new URL("/api/apps/manifest", input.issuer);
465
+ const credentials = Buffer.from(
466
+ `${input.clientId}:${input.clientSecret}`,
467
+ ).toString("base64");
468
+
469
+ const response = await portalFetch(endpoint.toString(), {
470
+ method: "POST",
471
+ headers: {
472
+ authorization: `Basic ${credentials}`,
473
+ "content-type": "application/json",
474
+ },
475
+ body: JSON.stringify({
476
+ permissions: input.permissions.map((key) => ({ key })),
477
+ roles: Object.entries(input.roles).map(([name, permissions]) => ({
478
+ name,
479
+ permissions,
480
+ })),
481
+ }),
482
+ });
483
+
484
+ const payload = (await response.json().catch(() => ({}))) as Record<
485
+ string,
486
+ unknown
487
+ >;
488
+
489
+ if (!response.ok) {
490
+ throw new SSOProtocolError(
491
+ `El portal rechazó el manifiesto (HTTP ${response.status}): ${String(payload.message ?? payload.error ?? "sin detalle")}`,
492
+ payload,
493
+ response.status,
494
+ );
495
+ }
496
+
497
+ return {
498
+ permissions: Number(payload.permissions ?? 0),
499
+ roles: Number(payload.roles ?? 0),
500
+ };
501
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  refreshOnce,
9
9
  refreshTokens,
10
10
  sessionFromPayload,
11
+ syncManifest,
11
12
  type TokenSet,
12
13
  } from "./core";
13
14
  import { isCredentialRejected, SSOError } from "./errors";
@@ -17,7 +18,7 @@ import {
17
18
  type SessionPayload,
18
19
  sealSession,
19
20
  } from "./session-cookie";
20
- import type { SSOClientOptions, SSOSession } from "./types";
21
+ import type { SSOClientOptions, SSOSession, SyncResult } from "./types";
21
22
 
22
23
  export {
23
24
  type ResolvedSSOOptions,
@@ -46,6 +47,7 @@ export type {
46
47
  SSOClientOptions,
47
48
  SSOSession,
48
49
  SSOUser,
50
+ SyncResult,
49
51
  } from "./types";
50
52
 
51
53
  /**
@@ -150,6 +152,15 @@ export type SSOClient<Permission extends string = string> = {
150
152
 
151
153
  /** Atiende las rutas del cliente. Devuelve `null` si la ruta no le compete. */
152
154
  handleRequest(request: Request): Promise<Response | null>;
155
+
156
+ /**
157
+ * Declara en el portal los `permissions` y `roles` de esta aplicación.
158
+ *
159
+ * Es idempotente y solo toca el catálogo de este cliente: llámalo en cada
160
+ * arranque (p. ej. desde `instrumentation.ts`) y la consola refleja siempre lo
161
+ * que el código exige. No asigna roles a cuentas.
162
+ */
163
+ sync(): Promise<SyncResult>;
153
164
  };
154
165
 
155
166
  /**
@@ -640,6 +651,15 @@ export function defineSSO<const Permission extends string = string>(
640
651
 
641
652
  resolveSession,
642
653
 
654
+ sync: () =>
655
+ syncManifest({
656
+ issuer: resolved.issuer,
657
+ clientId: resolved.clientId,
658
+ clientSecret: resolved.clientSecret,
659
+ permissions: resolved.permissions ?? [],
660
+ roles: resolved.roles ?? {},
661
+ }),
662
+
643
663
  async handleRequest(request) {
644
664
  const { pathname } = new URL(request.url);
645
665
  if (!pathname.startsWith(`${basePath}/`)) return null;
package/src/types.ts CHANGED
@@ -47,6 +47,17 @@ export type SSOClientOptions<Permission extends string = string> = {
47
47
  */
48
48
  permissions?: readonly Permission[];
49
49
 
50
+ /**
51
+ * Roles de la aplicación y los permisos que agrupa cada uno.
52
+ *
53
+ * Junto con `permissions`, es el manifiesto que `sso.sync()` envía al portal:
54
+ * declarar aquí evita copiarlos a mano en la consola. Los permisos de cada
55
+ * rol salen del mismo tipo que `permissions`, así que una errata no compila.
56
+ * Sincronizar solo declara el catálogo; a quién se asigna cada rol se sigue
57
+ * decidiendo en la consola.
58
+ */
59
+ roles?: Readonly<Record<string, readonly Permission[]>>;
60
+
50
61
  /** Ruta donde se montan los handlers. Por defecto `/api/sso`. */
51
62
  basePath?: string;
52
63
 
@@ -139,3 +150,6 @@ export type DiscoveryDocument = {
139
150
  revocation_endpoint?: string;
140
151
  introspection_endpoint?: string;
141
152
  };
153
+
154
+ /** Lo que el portal respondió al sincronizar el manifiesto. */
155
+ export type SyncResult = { permissions: number; roles: number };