@neetru/sdk 3.1.1 → 3.1.4

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 (82) hide show
  1. package/README.md +56 -5
  2. package/dist/auth.cjs +255 -18
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.d.cts +1 -1
  5. package/dist/auth.d.ts +1 -1
  6. package/dist/auth.mjs +255 -18
  7. package/dist/auth.mjs.map +1 -1
  8. package/dist/catalog.cjs.map +1 -1
  9. package/dist/catalog.d.cts +1 -1
  10. package/dist/catalog.d.ts +1 -1
  11. package/dist/catalog.mjs.map +1 -1
  12. package/dist/checkout.cjs +22 -7
  13. package/dist/checkout.cjs.map +1 -1
  14. package/dist/checkout.d.cts +1 -1
  15. package/dist/checkout.d.ts +1 -1
  16. package/dist/checkout.mjs +22 -7
  17. package/dist/checkout.mjs.map +1 -1
  18. package/dist/db-react.d.cts +1 -1
  19. package/dist/db-react.d.ts +1 -1
  20. package/dist/db.cjs +59 -3
  21. package/dist/db.cjs.map +1 -1
  22. package/dist/db.d.cts +1 -1
  23. package/dist/db.d.ts +1 -1
  24. package/dist/db.mjs +59 -3
  25. package/dist/db.mjs.map +1 -1
  26. package/dist/entitlements.cjs.map +1 -1
  27. package/dist/entitlements.d.cts +1 -1
  28. package/dist/entitlements.d.ts +1 -1
  29. package/dist/entitlements.mjs.map +1 -1
  30. package/dist/errors.cjs.map +1 -1
  31. package/dist/errors.d.cts +11 -6
  32. package/dist/errors.d.ts +11 -6
  33. package/dist/errors.mjs.map +1 -1
  34. package/dist/firestore-compat.cjs +15 -2
  35. package/dist/firestore-compat.cjs.map +1 -1
  36. package/dist/firestore-compat.d.cts +1 -1
  37. package/dist/firestore-compat.d.ts +1 -1
  38. package/dist/firestore-compat.mjs +15 -2
  39. package/dist/firestore-compat.mjs.map +1 -1
  40. package/dist/index.cjs +267 -22
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +2 -2
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.mjs +267 -22
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/mocks.cjs +25 -0
  47. package/dist/mocks.cjs.map +1 -1
  48. package/dist/mocks.d.cts +18 -1
  49. package/dist/mocks.d.ts +18 -1
  50. package/dist/mocks.mjs +25 -0
  51. package/dist/mocks.mjs.map +1 -1
  52. package/dist/notifications.cjs.map +1 -1
  53. package/dist/notifications.d.cts +1 -1
  54. package/dist/notifications.d.ts +1 -1
  55. package/dist/notifications.mjs.map +1 -1
  56. package/dist/react.cjs.map +1 -1
  57. package/dist/react.d.cts +1 -1
  58. package/dist/react.d.ts +1 -1
  59. package/dist/react.mjs.map +1 -1
  60. package/dist/support.cjs.map +1 -1
  61. package/dist/support.d.cts +1 -1
  62. package/dist/support.d.ts +1 -1
  63. package/dist/support.mjs.map +1 -1
  64. package/dist/telemetry.cjs +2 -1
  65. package/dist/telemetry.cjs.map +1 -1
  66. package/dist/telemetry.d.cts +1 -1
  67. package/dist/telemetry.d.ts +1 -1
  68. package/dist/telemetry.mjs +2 -1
  69. package/dist/telemetry.mjs.map +1 -1
  70. package/dist/{types-B3XFHD4A.d.ts → types-CRIIjqzC.d.ts} +105 -12
  71. package/dist/{types-DePdSU3P.d.cts → types-DvMdYbfV.d.cts} +105 -12
  72. package/dist/usage.cjs.map +1 -1
  73. package/dist/usage.d.cts +1 -1
  74. package/dist/usage.d.ts +1 -1
  75. package/dist/usage.mjs.map +1 -1
  76. package/dist/webhooks.cjs +11 -3
  77. package/dist/webhooks.cjs.map +1 -1
  78. package/dist/webhooks.d.cts +1 -1
  79. package/dist/webhooks.d.ts +1 -1
  80. package/dist/webhooks.mjs +11 -3
  81. package/dist/webhooks.mjs.map +1 -1
  82. package/package.json +1 -1
package/README.md CHANGED
@@ -77,6 +77,42 @@ if (!check.allowed && check.behavior === 'readonly') {
77
77
 
78
78
  ## Exemplos por namespace
79
79
 
80
+ ### Login OIDC — Authorization Code + PKCE (3.1.3)
81
+
82
+ O SDK implementa o lado-**browser** do fluxo; a troca `code → tokens` acontece no
83
+ **backend do produto** (confidential client com `client_id` + `client_secret`
84
+ registrados em `oidc_clients/{id}` no Core — modelo Firebase/Facebook).
85
+
86
+ ```ts
87
+ // 1) Browser — iniciar login (gera state CSRF + nonce + PKCE S256 e redireciona)
88
+ const neetru = createNeetruClient({ oidcClientId: 'meu-app-oidc' });
89
+ await neetru.auth.signIn({
90
+ redirectUri: 'https://app.exemplo.com/auth/callback',
91
+ postLoginRedirect: '/dashboard',
92
+ });
93
+
94
+ // 2) Browser — na página de callback: valida state e devolve o material do exchange
95
+ const cb = await neetru.auth.handleRedirectCallback();
96
+ if (cb) {
97
+ // mande pro SEU backend (nunca exponha client_secret no browser)
98
+ await fetch('/api/auth/exchange', { method: 'POST', body: JSON.stringify(cb) });
99
+ location.assign(cb.postLoginRedirect ?? '/');
100
+ }
101
+
102
+ // 3) Backend do produto — troca o code por tokens no IdP Neetru
103
+ // POST https://auth.neetru.com/api/v1/oauth/token (x-www-form-urlencoded)
104
+ // Authorization: Basic base64(clientId:clientSecret)
105
+ // grant_type=authorization_code & code & redirect_uri & code_verifier
106
+ // → valide claim `nonce` do id_token === cb.nonce; sete cookie httpOnly.
107
+
108
+ // 4) Server-side — valide o id_token em qualquer request (JWKS cacheado)
109
+ const user = await neetru.auth.verifyToken(idTokenDoCookie);
110
+ ```
111
+
112
+ `auth.getIdToken()` expõe o JWT cru se o app optar por armazená-lo em
113
+ `localStorage['neetru_id_token']` (alternativa SPA; o padrão recomendado é o
114
+ cookie httpOnly do backend).
115
+
80
116
  ### Webhooks outbound (v1.2)
81
117
 
82
118
  ```ts
@@ -95,17 +131,20 @@ Eventos recebidos no seu endpoint chegam com:
95
131
  - `X-Neetru-Signature: sha256=<hmac>` (se `secret` registrado)
96
132
  - `X-Neetru-Timestamp: <ms>` (replay protection — rejeitar > 5min skew)
97
133
 
98
- #### Verificar assinatura no consumer (3.0 — async + Universal)
134
+ #### Verificar assinatura no consumer (3.1.3 — async + Universal)
135
+
136
+ O HMAC do Core cobre `` `${timestamp}.${rawBody}` `` (o timestamp é o valor de
137
+ `X-Neetru-Timestamp`). Prefira `verifyWebhookRequest`, que lê os headers,
138
+ valida o skew (5 min) e o HMAC de uma vez:
99
139
 
100
140
  ```ts
101
- import { verifyWebhookSignature } from '@neetru/sdk';
141
+ import { verifyWebhookRequest } from '@neetru/sdk';
102
142
 
103
143
  // Next.js Route Handler (Edge OU Node runtime — funciona em ambos)
104
144
  export async function POST(req: Request) {
105
145
  const raw = await req.text();
106
- const sig = req.headers.get('X-Neetru-Signature');
107
- const ok = await verifyWebhookSignature(raw, sig, process.env.NEETRU_WEBHOOK_SECRET!);
108
- if (!ok) return new Response('unauthorized', { status: 401 });
146
+ const result = await verifyWebhookRequest(raw, req.headers, process.env.NEETRU_WEBHOOK_SECRET!);
147
+ if (!result.ok) return new Response('unauthorized', { status: 401 });
109
148
 
110
149
  const event = JSON.parse(raw);
111
150
  // ... handle event
@@ -113,7 +152,19 @@ export async function POST(req: Request) {
113
152
  }
114
153
  ```
115
154
 
155
+ Verificação manual com `verifyWebhookSignature` exige passar o timestamp:
156
+
157
+ ```ts
158
+ const ok = await verifyWebhookSignature(
159
+ raw,
160
+ req.headers.get('X-Neetru-Signature'),
161
+ process.env.NEETRU_WEBHOOK_SECRET!,
162
+ req.headers.get('X-Neetru-Timestamp') ?? undefined,
163
+ );
164
+ ```
165
+
116
166
  > **Breaking 3.0**: era `boolean` sync usando `node:crypto`. Agora `Promise<boolean>` via WebCrypto.subtle — funciona em Cloudflare Workers, Vercel Edge, browsers e Node ≥20.
167
+ > **Fix 3.1.3**: até 3.1.2 o helper verificava o HMAC só sobre o body — entregas reais do Core (assinadas com `timestamp.body`) sempre falhavam. Passe o `timestamp` (ou use `verifyWebhookRequest`).
117
168
 
118
169
  ### Notifications produto → user (v1.2)
119
170
 
package/dist/auth.cjs CHANGED
@@ -1095,7 +1095,8 @@ function createTelemetryNamespace(config) {
1095
1095
  message: input.message
1096
1096
  };
1097
1097
  if (input.metadata) body.metadata = input.metadata;
1098
- if (input.productSlug) body.productSlug = input.productSlug;
1098
+ const productSlug = input.productSlug ?? config.productId;
1099
+ if (productSlug) body.productSlug = productSlug;
1099
1100
  let cid = input.correlationId;
1100
1101
  if (!cid) {
1101
1102
  try {
@@ -4603,13 +4604,69 @@ function createRestSyncTransport(config) {
4603
4604
  if (!config) {
4604
4605
  return { docs: [], newWatermark: Date.now(), resyncRequired: false };
4605
4606
  }
4606
- return { docs: [], newWatermark: Date.now(), resyncRequired: false };
4607
+ return { docs: [], newWatermark: Date.now(), resyncRequired: true };
4607
4608
  },
4608
- async fullResync(_collections) {
4609
+ async fullResync(collections) {
4609
4610
  if (!config) {
4610
4611
  return { docs: [], newWatermark: Date.now() };
4611
4612
  }
4612
- return { docs: [], newWatermark: Date.now() };
4613
+ const { httpRequest: httpRequest2 } = await Promise.resolve().then(() => (init_http(), http_exports));
4614
+ const tenantScopeId = config.tenantId ?? config.productId;
4615
+ const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
4616
+ const now = Date.now();
4617
+ const docs = [];
4618
+ for (const collection of collections) {
4619
+ let raw;
4620
+ try {
4621
+ raw = await httpRequest2(config, {
4622
+ method: "GET",
4623
+ path: DATASTORE_COLLECTION_ENDPOINT(collection),
4624
+ requireAuth: true,
4625
+ retries: 2,
4626
+ headers: tenantHeaders,
4627
+ // Teto do Core é 200. Coleções > 200 docs truncam silenciosamente
4628
+ // aqui (paginação cursor é tarefa Core separada) — a detecção de
4629
+ // deleção-por-ausência do SyncEngine NÃO deve rodar sobre truncamento,
4630
+ // então mantemos o limite no máximo do servidor por ora.
4631
+ query: { limit: 200 }
4632
+ });
4633
+ } catch (err) {
4634
+ const msg = err instanceof Error ? err.message : String(err);
4635
+ throw new NeetruDbError(
4636
+ "db_unavailable",
4637
+ `[RestSyncTransport] fullResync falhou na cole\xE7\xE3o "${collection}": ${msg}`
4638
+ );
4639
+ }
4640
+ const resp = raw;
4641
+ const items = Array.isArray(resp?.items) ? resp.items : [];
4642
+ for (const item of items) {
4643
+ if (!item || typeof item !== "object") continue;
4644
+ const record = item;
4645
+ const id = typeof record["id"] === "string" ? record["id"] : null;
4646
+ if (!id) continue;
4647
+ const { id: _inlinedId, ...data } = record;
4648
+ let serverTimestamp = now;
4649
+ const updatedAt = data["_updatedAt"];
4650
+ if (typeof updatedAt === "number") {
4651
+ serverTimestamp = updatedAt;
4652
+ } else if (updatedAt !== null && typeof updatedAt === "object" && typeof updatedAt["_seconds"] === "number") {
4653
+ serverTimestamp = updatedAt["_seconds"] * 1e3;
4654
+ } else if (typeof updatedAt === "string") {
4655
+ const parsed = Date.parse(updatedAt);
4656
+ if (!Number.isNaN(parsed)) serverTimestamp = parsed;
4657
+ }
4658
+ const serverVersion = `rest_${id}_${serverTimestamp}`;
4659
+ docs.push({
4660
+ collection,
4661
+ id,
4662
+ data,
4663
+ serverVersion,
4664
+ serverTimestamp,
4665
+ deleted: false
4666
+ });
4667
+ }
4668
+ }
4669
+ return { docs, newWatermark: now };
4613
4670
  }
4614
4671
  };
4615
4672
  }
@@ -4849,6 +4906,15 @@ function createLazyCollectionRef(name, getDocsPromise) {
4849
4906
  // src/checkout.ts
4850
4907
  init_errors();
4851
4908
  init_http();
4909
+ function checkoutAuth(accessToken, method) {
4910
+ if (!accessToken || typeof accessToken !== "string") {
4911
+ throw new NeetruError(
4912
+ "validation_failed",
4913
+ `checkout.${method} requer o access_token OIDC do usu\xE1rio final (passe \`accessToken\`). A chave de API do SDK (nrt_*) N\xC3O \xE9 aceita pelo endpoint de checkout \u2014 ele autentica a identidade do cliente que est\xE1 assinando (Bearer access_token OIDC ou cookie de sess\xE3o).`
4914
+ );
4915
+ }
4916
+ return { requireAuth: false, headers: { authorization: `Bearer ${accessToken}` } };
4917
+ }
4852
4918
  function parseStartResponse(raw) {
4853
4919
  if (!raw || typeof raw !== "object") {
4854
4920
  throw new NeetruError("invalid_response", "checkout.start response is not an object");
@@ -4926,11 +4992,13 @@ function createHttpCheckoutNamespace(config) {
4926
4992
  };
4927
4993
  if (input.tenantType) body.targetTenantType = input.tenantType;
4928
4994
  if (input.tenantId) body.targetTenantId = input.tenantId;
4995
+ const { requireAuth, headers } = checkoutAuth(input.accessToken, "start");
4929
4996
  const raw = await httpRequest(config, {
4930
4997
  method: "POST",
4931
4998
  path: "/api/v1/checkout/intents",
4932
4999
  body,
4933
- requireAuth: true,
5000
+ requireAuth,
5001
+ headers,
4934
5002
  idempotencyKey: true
4935
5003
  });
4936
5004
  const result = parseStartResponse(raw);
@@ -4940,25 +5008,29 @@ function createHttpCheckoutNamespace(config) {
4940
5008
  }
4941
5009
  return result;
4942
5010
  },
4943
- async get(intentId) {
5011
+ async get(intentId, opts) {
4944
5012
  if (!intentId || typeof intentId !== "string") {
4945
5013
  throw new NeetruError("validation_failed", "checkout.get: intentId is required");
4946
5014
  }
5015
+ const { requireAuth, headers } = checkoutAuth(opts?.accessToken, "get");
4947
5016
  const raw = await httpRequest(config, {
4948
5017
  method: "GET",
4949
5018
  path: `/api/v1/checkout/intents/${encodeURIComponent(intentId)}`,
4950
- requireAuth: true
5019
+ requireAuth,
5020
+ headers
4951
5021
  });
4952
5022
  return parseGetResponse(raw);
4953
5023
  },
4954
- async cancel(intentId) {
5024
+ async cancel(intentId, opts) {
4955
5025
  if (!intentId || typeof intentId !== "string") {
4956
5026
  throw new NeetruError("validation_failed", "checkout.cancel: intentId is required");
4957
5027
  }
5028
+ const { requireAuth, headers } = checkoutAuth(opts?.accessToken, "cancel");
4958
5029
  const raw = await httpRequest(config, {
4959
5030
  method: "DELETE",
4960
5031
  path: `/api/v1/checkout/intents/${encodeURIComponent(intentId)}`,
4961
- requireAuth: true
5032
+ requireAuth,
5033
+ headers
4962
5034
  });
4963
5035
  return {
4964
5036
  ok: true,
@@ -5002,14 +5074,14 @@ var MockCheckout = class {
5002
5074
  requiresKyc: false
5003
5075
  };
5004
5076
  }
5005
- async get(intentId) {
5077
+ async get(intentId, _opts) {
5006
5078
  const found = this.intents.get(intentId);
5007
5079
  if (!found) {
5008
5080
  throw new NeetruError("not_found", `Mock intent ${intentId} not found`);
5009
5081
  }
5010
5082
  return { ...found };
5011
5083
  }
5012
- async cancel(intentId) {
5084
+ async cancel(intentId, _opts) {
5013
5085
  const found = this.intents.get(intentId);
5014
5086
  if (!found) {
5015
5087
  throw new NeetruError("not_found", `Mock intent ${intentId} not found`);
@@ -5464,6 +5536,31 @@ var MockAuth = class {
5464
5536
  if (this._user) return this._user;
5465
5537
  throw Object.assign(new Error("Nenhum user mock dispon\xEDvel"), { code: "token_invalid_signature" });
5466
5538
  }
5539
+ /**
5540
+ * Mock de `handleRedirectCallback` (Bug 2 / 3.1.3) — não há redirect real no
5541
+ * mock, então retorna `null` por default. Use `__setPendingCallback(result)`
5542
+ * em testes pra simular um retorno do IdP.
5543
+ */
5544
+ async handleRedirectCallback(_currentUrl) {
5545
+ return this._pendingCallback;
5546
+ }
5547
+ /**
5548
+ * Mock de `getIdToken` (Bug 2 / 3.1.3). Retorna `null` por default; use
5549
+ * `__setIdToken(token)` pra injetar um token cru em testes.
5550
+ */
5551
+ getIdToken() {
5552
+ return this._idToken;
5553
+ }
5554
+ _pendingCallback = null;
5555
+ _idToken = null;
5556
+ /** Test helper — simula o material devolvido por um callback OIDC. */
5557
+ __setPendingCallback(result) {
5558
+ this._pendingCallback = result;
5559
+ }
5560
+ /** Test helper — injeta o id_token cru retornado por `getIdToken`. */
5561
+ __setIdToken(token) {
5562
+ this._idToken = token;
5563
+ }
5467
5564
  _verifyTokenOverride = null;
5468
5565
  /**
5469
5566
  * Injeta override pra `verifyToken` em testes — permite simular expiração,
@@ -5682,6 +5779,49 @@ function _jwtPayloadToUser(payload) {
5682
5779
  ...payload
5683
5780
  };
5684
5781
  }
5782
+ var OIDC_TX_KEY = "neetru_oauth_tx";
5783
+ function resolveIdpOrigin(baseUrl) {
5784
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5785
+ return overrideAuthUrl ?? baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5786
+ }
5787
+ function getWebCrypto() {
5788
+ const c = globalThis.crypto;
5789
+ if (!c || typeof c.getRandomValues !== "function" || !c.subtle) {
5790
+ throw new NeetruError(
5791
+ "runtime_unsupported",
5792
+ "auth.signIn requer WebCrypto (crypto.subtle) \u2014 indispon\xEDvel neste runtime. Use HTTPS (ou http://localhost) num browser moderno."
5793
+ );
5794
+ }
5795
+ return c;
5796
+ }
5797
+ function base64UrlFromBytes(bytes) {
5798
+ let str = "";
5799
+ for (const b of bytes) str += String.fromCharCode(b);
5800
+ const b64 = typeof btoa === "function" ? btoa(str) : Buffer.from(str, "binary").toString("base64");
5801
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
5802
+ }
5803
+ function randomUrlSafe(crypto2, byteLen) {
5804
+ const bytes = new Uint8Array(byteLen);
5805
+ crypto2.getRandomValues(bytes);
5806
+ return base64UrlFromBytes(bytes);
5807
+ }
5808
+ async function pkceChallengeS256(crypto2, verifier) {
5809
+ const data = new TextEncoder().encode(verifier);
5810
+ const digest = await crypto2.subtle.digest("SHA-256", data);
5811
+ return base64UrlFromBytes(new Uint8Array(digest));
5812
+ }
5813
+ function cleanCallbackUrl(parsed) {
5814
+ try {
5815
+ const history = globalThis.history;
5816
+ if (!history || typeof history.replaceState !== "function") return;
5817
+ const clean = new URL(parsed.toString());
5818
+ for (const p of ["code", "state", "error", "error_description", "iss", "session_state"]) {
5819
+ clean.searchParams.delete(p);
5820
+ }
5821
+ history.replaceState(history.state ?? null, "", clean.pathname + clean.search + clean.hash);
5822
+ } catch {
5823
+ }
5824
+ }
5685
5825
  function createOidcAuthNamespace(config) {
5686
5826
  const STORAGE_KEY = "neetru_id_token";
5687
5827
  const listeners = /* @__PURE__ */ new Set();
@@ -5694,6 +5834,13 @@ function createOidcAuthNamespace(config) {
5694
5834
  return null;
5695
5835
  }
5696
5836
  }
5837
+ function getSessionStorage() {
5838
+ try {
5839
+ return typeof globalThis.sessionStorage !== "undefined" ? globalThis.sessionStorage : null;
5840
+ } catch {
5841
+ return null;
5842
+ }
5843
+ }
5697
5844
  function decodeJwtPayload(token) {
5698
5845
  const parts = token.split(".");
5699
5846
  if (parts.length !== 3) return null;
@@ -5777,20 +5924,48 @@ function createOidcAuthNamespace(config) {
5777
5924
  return {
5778
5925
  async signIn(options) {
5779
5926
  if (typeof globalThis.location !== "undefined" && typeof globalThis.location.assign === "function") {
5927
+ const crypto2 = getWebCrypto();
5780
5928
  const redirectUri = options?.redirectUri ?? globalThis.location.origin;
5781
5929
  const scope = options?.scope ?? "openid profile email";
5782
- const state = options?.postLoginRedirect ?? globalThis.location.href;
5783
- const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
5784
- const idpOrigin = overrideAuthUrl ?? config.baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
5930
+ let clientId = config.oidcClientId;
5931
+ if (!clientId && config.apiKey) {
5932
+ clientId = parseApiKey(config.apiKey).keyId;
5933
+ }
5934
+ if (!clientId) {
5935
+ throw new NeetruError(
5936
+ "invalid_config",
5937
+ "auth.signIn requer `oidcClientId` (ou `apiKey`) configurado para montar o client_id."
5938
+ );
5939
+ }
5940
+ const state = randomUrlSafe(crypto2, 16);
5941
+ const nonce = randomUrlSafe(crypto2, 16);
5942
+ const codeVerifier = randomUrlSafe(crypto2, 32);
5943
+ const codeChallenge = await pkceChallengeS256(crypto2, codeVerifier);
5944
+ const session = getSessionStorage();
5945
+ if (!session) {
5946
+ throw new NeetruError(
5947
+ "runtime_unsupported",
5948
+ "auth.signIn requer sessionStorage para guardar a transa\xE7\xE3o PKCE entre o redirect e o callback."
5949
+ );
5950
+ }
5951
+ const tx = {
5952
+ state,
5953
+ nonce,
5954
+ codeVerifier,
5955
+ redirectUri,
5956
+ postLoginRedirect: options?.postLoginRedirect
5957
+ };
5958
+ session.setItem(OIDC_TX_KEY, JSON.stringify(tx));
5959
+ const idpOrigin = resolveIdpOrigin(config.baseUrl);
5785
5960
  const url = new URL("/api/v1/oauth/authorize", idpOrigin);
5786
5961
  url.searchParams.set("response_type", "code");
5962
+ url.searchParams.set("client_id", clientId);
5787
5963
  url.searchParams.set("redirect_uri", redirectUri);
5788
5964
  url.searchParams.set("scope", scope);
5789
5965
  url.searchParams.set("state", state);
5790
- if (config.apiKey) {
5791
- const { keyId } = parseApiKey(config.apiKey);
5792
- url.searchParams.set("client_id", keyId);
5793
- }
5966
+ url.searchParams.set("nonce", nonce);
5967
+ url.searchParams.set("code_challenge", codeChallenge);
5968
+ url.searchParams.set("code_challenge_method", "S256");
5794
5969
  globalThis.location.assign(url.toString());
5795
5970
  return;
5796
5971
  }
@@ -5799,6 +5974,67 @@ function createOidcAuthNamespace(config) {
5799
5974
  "auth.signIn requires a browser context or mocks. Use NEETRU_ENV=dev or pass mocks.auth."
5800
5975
  );
5801
5976
  },
5977
+ async handleRedirectCallback(currentUrl) {
5978
+ const href = currentUrl ?? (typeof globalThis.location !== "undefined" ? globalThis.location.href : void 0);
5979
+ if (!href) return null;
5980
+ let parsed;
5981
+ try {
5982
+ parsed = new URL(href);
5983
+ } catch {
5984
+ return null;
5985
+ }
5986
+ const params = parsed.searchParams;
5987
+ const idpError = params.get("error");
5988
+ if (idpError) {
5989
+ const desc = params.get("error_description") ?? idpError;
5990
+ getSessionStorage()?.removeItem(OIDC_TX_KEY);
5991
+ cleanCallbackUrl(parsed);
5992
+ throw new NeetruError(idpError, `OIDC authorize falhou: ${desc}`);
5993
+ }
5994
+ const code = params.get("code");
5995
+ if (!code) return null;
5996
+ const returnedState = params.get("state");
5997
+ const session = getSessionStorage();
5998
+ const txRaw = session?.getItem(OIDC_TX_KEY) ?? null;
5999
+ let tx = null;
6000
+ if (txRaw) {
6001
+ try {
6002
+ tx = JSON.parse(txRaw);
6003
+ } catch {
6004
+ tx = null;
6005
+ }
6006
+ }
6007
+ if (!tx) {
6008
+ throw new NeetruError(
6009
+ "oidc_state_mismatch",
6010
+ "Callback OIDC sem transa\xE7\xE3o pendente \u2014 `signIn` n\xE3o foi iniciado nesta sess\xE3o (ou j\xE1 consumido)."
6011
+ );
6012
+ }
6013
+ if (!returnedState || returnedState !== tx.state) {
6014
+ session?.removeItem(OIDC_TX_KEY);
6015
+ cleanCallbackUrl(parsed);
6016
+ throw new NeetruError(
6017
+ "oidc_state_mismatch",
6018
+ "state do callback OIDC n\xE3o confere com o da transa\xE7\xE3o (poss\xEDvel CSRF)."
6019
+ );
6020
+ }
6021
+ session?.removeItem(OIDC_TX_KEY);
6022
+ cleanCallbackUrl(parsed);
6023
+ return {
6024
+ code,
6025
+ codeVerifier: tx.codeVerifier,
6026
+ redirectUri: tx.redirectUri,
6027
+ nonce: tx.nonce,
6028
+ state: tx.state,
6029
+ postLoginRedirect: tx.postLoginRedirect
6030
+ };
6031
+ },
6032
+ getIdToken() {
6033
+ const storage = getStorage();
6034
+ const token = storage?.getItem(STORAGE_KEY) ?? null;
6035
+ if (!token) return null;
6036
+ return tokenToUser(token) ? token : null;
6037
+ },
5802
6038
  async signOut() {
5803
6039
  const storage = getStorage();
5804
6040
  const storedToken = storage?.getItem(STORAGE_KEY) ?? null;
@@ -5905,6 +6141,7 @@ function createNeetruClient(config = {}) {
5905
6141
  const env = resolveEnv(config.env);
5906
6142
  const resolved = Object.freeze({
5907
6143
  apiKey,
6144
+ oidcClientId: config.oidcClientId,
5908
6145
  baseUrl,
5909
6146
  fetch: fetchImpl.bind(globalThis),
5910
6147
  env,