@neetru/sdk 3.5.0 → 3.6.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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable changes to `@neetru/sdk` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.6.0] - 2026-09-24 — **feat(mobile): `@neetru/sdk/mobile` — namespace de auth OIDC+PKCE para React Native/Expo**
9
+
10
+ Release **minor** (capacidade nova, aditiva — subpath novo, nenhuma API existente muda).
11
+
12
+ ### Contexto
13
+
14
+ O namespace `auth` existente (`createNeetruClient(...).auth`) é hardcoded em três APIs que não existem em React Native: `globalThis.location.assign(url)` pra iniciar o redirect do `signIn`, e `sessionStorage`/`localStorage` pra persistir a transação PKCE e os tokens. Não havia seam de injeção pra nenhum dos três, então nenhum app RN conseguia usar login Neetru via o SDK — cada squad de app mobile reimplementaria PKCE do zero (risco de bug criptográfico duplicado).
15
+
16
+ ### Adicionado (aditivo, novo subpath — não muda nada em `.`/`./auth`)
17
+
18
+ - **`@neetru/sdk/mobile`** — `createMobileAuthNamespace(config)`, com `config.storage` (`MobileAuthStorage` assíncrono — o app injeta `expo-secure-store`/Keychain/etc) e `config.openAuthUrl` (o app injeta `expo-web-browser`/`Linking.openURL` — **browser externo, nunca WebView embutida**, requisito de segurança RFC 8252 §8.12, documentado no JSDoc do módulo).
19
+ - Namespace expõe `signIn(options)`, `completeSignIn(callbackUrl)` (recebe a URL do deep link explicitamente — mobile não tem `location.href` implícito), `getIdToken()`/`getUser()` (**assíncronos** — contrato diferente do `auth` web, porque `config.storage` é sempre `Promise`), `onAuthStateChanged(listener)`, `refreshTokens()`, `signOut()`.
20
+ - **`src/auth-pkce-shared.ts`** (interno, não exportado publicamente) — a lógica criptográfica PKCE (WebCrypto, geração `state`/`nonce`/`code_verifier`/`code_challenge` S256, parsing/validação de `id_token`) foi extraída do `auth.ts` web pra ser reaproveitada 1:1 por `mobile.ts`, em vez de duplicada por copy-paste. `auth.ts` foi refatorado pra importar dali — comportamento idêntico, coberto pelos 78 testes pré-existentes de `client.auth`, que continuam verdes sem alteração.
21
+
22
+ ### Requisitos do app consumidor (documentados no JSDoc do módulo)
23
+
24
+ - Polyfill de WebCrypto (`react-native-get-random-values` + `react-native-quick-crypto` ou equivalente) — Hermes não tem `crypto.subtle` nativo. Sem o polyfill, `signIn`/`completeSignIn` lançam `NeetruError('runtime_unsupported', ...)` com mensagem clara — o SDK não tenta poliflar sozinho nem gera PKCE fraco.
25
+ - `config.openAuthUrl` deve abrir um browser externo (`ASWebAuthenticationSession`/Custom Tabs via `expo-web-browser`, ou `Linking.openURL`) — nunca uma WebView in-app.
26
+ - `client.db` (offline-first, IndexedDB) **não tem storage persistente equivalente em React Native ainda** — cai no `MemoryStore` in-memory (não sobrevive a restart do app). Limitação conhecida e aceita de v1, não resolvida por este subpath.
27
+
28
+ Nenhuma remoção de API, nenhuma mudança de wire nos namespaces existentes.
29
+
8
30
  ## [3.5.0] - 2026-09-19 — **feat(http): header `X-Neetru-Sdk-Version` em toda request**
9
31
 
10
32
  Release **minor** (capacidade nova, sem quebra de contrato existente).
@@ -28,7 +50,6 @@ Nenhuma remoção de API, nenhuma mudança de wire nos endpoints existentes —
28
50
  - **Remoção de código morto** — deletado `src/db.ts` legado (namespace v0.3.1 Sprint 8, sem nenhum import; o namespace `db` público vive em `src/db/client-db.ts`).
29
51
  - Doc: exemplo do JSDoc em `auth.ts` corrigido de `usage.track(...)` (API inexistente) para `usage.report(...)`.
30
52
  - Tooling: script `test:coverage` + devDeps `@testing-library/dom` e `@vitest/coverage-v8`.
31
-
32
53
  ## [3.4.7] - 2026-09-19 — **fix(a11y): NeetruLoginButton — foco de teclado visível (bug_62edcfb0)**
33
54
 
34
55
  Release **patch** (correção de bug de acessibilidade — publica o fix já mesclado no PR #28).
package/dist/ai.cjs CHANGED
@@ -47,7 +47,7 @@ function compareCalVer(a, b) {
47
47
  }
48
48
  function readInjectedSdkVersion() {
49
49
  try {
50
- return "3.5.0";
50
+ return "3.6.0";
51
51
  } catch {
52
52
  return void 0;
53
53
  }
package/dist/ai.mjs CHANGED
@@ -45,7 +45,7 @@ function compareCalVer(a, b) {
45
45
  }
46
46
  function readInjectedSdkVersion() {
47
47
  try {
48
- return "3.5.0";
48
+ return "3.6.0";
49
49
  } catch {
50
50
  return void 0;
51
51
  }
package/dist/auth.cjs CHANGED
@@ -83,7 +83,7 @@ function compareCalVer(a, b) {
83
83
  }
84
84
  function readInjectedSdkVersion() {
85
85
  try {
86
- return "3.5.0";
86
+ return "3.6.0";
87
87
  } catch {
88
88
  return void 0;
89
89
  }
@@ -6990,6 +6990,94 @@ var MockAi = class {
6990
6990
  }
6991
6991
  };
6992
6992
 
6993
+ // src/auth-pkce-shared.ts
6994
+ init_errors();
6995
+ function getWebCrypto() {
6996
+ const c = globalThis.crypto;
6997
+ if (!c || typeof c.getRandomValues !== "function" || !c.subtle) {
6998
+ throw new NeetruError(
6999
+ "runtime_unsupported",
7000
+ "auth.signIn requer WebCrypto (crypto.subtle) \u2014 indispon\xEDvel neste runtime. Use HTTPS (ou http://localhost) num browser moderno, ou instale um polyfill WebCrypto (ex: react-native-quick-crypto) em React Native."
7001
+ );
7002
+ }
7003
+ return c;
7004
+ }
7005
+ function base64UrlFromBytes(bytes) {
7006
+ let str = "";
7007
+ for (const b of bytes) str += String.fromCharCode(b);
7008
+ const b64 = typeof btoa === "function" ? btoa(str) : Buffer.from(str, "binary").toString("base64");
7009
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
7010
+ }
7011
+ function randomUrlSafe(crypto2, byteLen) {
7012
+ const bytes = new Uint8Array(byteLen);
7013
+ crypto2.getRandomValues(bytes);
7014
+ return base64UrlFromBytes(bytes);
7015
+ }
7016
+ async function pkceChallengeS256(crypto2, verifier) {
7017
+ const data = new TextEncoder().encode(verifier);
7018
+ const digest = await crypto2.subtle.digest("SHA-256", data);
7019
+ return base64UrlFromBytes(new Uint8Array(digest));
7020
+ }
7021
+ function resolveIdpOrigin(baseUrl) {
7022
+ const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
7023
+ return overrideAuthUrl ?? baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
7024
+ }
7025
+ function resolveOidcClientId(config) {
7026
+ let clientId = config.oidcClientId;
7027
+ if (!clientId && config.apiKey) {
7028
+ clientId = parseApiKey(config.apiKey).keyId;
7029
+ }
7030
+ if (!clientId) {
7031
+ throw new NeetruError(
7032
+ "invalid_config",
7033
+ "auth OIDC requer `oidcClientId` (ou `apiKey`) configurado para montar o client_id."
7034
+ );
7035
+ }
7036
+ return clientId;
7037
+ }
7038
+ function buildAuthorizeUrl(params) {
7039
+ const url = new URL("/api/v1/oauth/authorize", params.idpOrigin);
7040
+ url.searchParams.set("response_type", "code");
7041
+ url.searchParams.set("client_id", params.clientId);
7042
+ url.searchParams.set("redirect_uri", params.redirectUri);
7043
+ url.searchParams.set("scope", params.scope);
7044
+ url.searchParams.set("state", params.state);
7045
+ url.searchParams.set("nonce", params.nonce);
7046
+ url.searchParams.set("code_challenge", params.codeChallenge);
7047
+ url.searchParams.set("code_challenge_method", "S256");
7048
+ return url;
7049
+ }
7050
+ function decodeJwtPayload(token) {
7051
+ const parts = token.split(".");
7052
+ if (parts.length !== 3) return null;
7053
+ try {
7054
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
7055
+ const pad = "=".repeat((4 - b64.length % 4) % 4);
7056
+ const decoded = typeof atob === "function" ? atob(b64 + pad) : Buffer.from(b64 + pad, "base64").toString("utf-8");
7057
+ return JSON.parse(decoded);
7058
+ } catch {
7059
+ return null;
7060
+ }
7061
+ }
7062
+ function jwtToNeetruUser(token) {
7063
+ const payload = decodeJwtPayload(token);
7064
+ if (!payload || typeof payload.sub !== "string") return null;
7065
+ if (typeof payload.exp === "number" && payload.exp * 1e3 < Date.now()) {
7066
+ return null;
7067
+ }
7068
+ return {
7069
+ uid: payload.sub,
7070
+ email: typeof payload.email === "string" ? payload.email : "",
7071
+ emailVerified: typeof payload.email_verified === "boolean" ? payload.email_verified : void 0,
7072
+ displayName: typeof payload.name === "string" ? payload.name : void 0,
7073
+ photoURL: typeof payload.picture === "string" ? payload.picture : void 0,
7074
+ isStaff: payload.is_staff === true,
7075
+ isCustomer: payload.is_customer === true,
7076
+ tenantId: typeof payload.tenant_id === "string" ? payload.tenant_id : void 0,
7077
+ ...payload
7078
+ };
7079
+ }
7080
+
6993
7081
  // src/auth.ts
6994
7082
  function readEnvApiKey() {
6995
7083
  try {
@@ -7047,36 +7135,6 @@ function _jwtPayloadToUser(payload) {
7047
7135
  };
7048
7136
  }
7049
7137
  var OIDC_TX_KEY = "neetru_oauth_tx";
7050
- function resolveIdpOrigin(baseUrl) {
7051
- const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
7052
- return overrideAuthUrl ?? baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
7053
- }
7054
- function getWebCrypto() {
7055
- const c = globalThis.crypto;
7056
- if (!c || typeof c.getRandomValues !== "function" || !c.subtle) {
7057
- throw new NeetruError(
7058
- "runtime_unsupported",
7059
- "auth.signIn requer WebCrypto (crypto.subtle) \u2014 indispon\xEDvel neste runtime. Use HTTPS (ou http://localhost) num browser moderno."
7060
- );
7061
- }
7062
- return c;
7063
- }
7064
- function base64UrlFromBytes(bytes) {
7065
- let str = "";
7066
- for (const b of bytes) str += String.fromCharCode(b);
7067
- const b64 = typeof btoa === "function" ? btoa(str) : Buffer.from(str, "binary").toString("base64");
7068
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
7069
- }
7070
- function randomUrlSafe(crypto2, byteLen) {
7071
- const bytes = new Uint8Array(byteLen);
7072
- crypto2.getRandomValues(bytes);
7073
- return base64UrlFromBytes(bytes);
7074
- }
7075
- async function pkceChallengeS256(crypto2, verifier) {
7076
- const data = new TextEncoder().encode(verifier);
7077
- const digest = await crypto2.subtle.digest("SHA-256", data);
7078
- return base64UrlFromBytes(new Uint8Array(digest));
7079
- }
7080
7138
  function cleanCallbackUrl(parsed) {
7081
7139
  try {
7082
7140
  const history = globalThis.history;
@@ -7110,36 +7168,6 @@ function createOidcAuthNamespace(config) {
7110
7168
  return null;
7111
7169
  }
7112
7170
  }
7113
- function decodeJwtPayload(token) {
7114
- const parts = token.split(".");
7115
- if (parts.length !== 3) return null;
7116
- try {
7117
- const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
7118
- const pad = "=".repeat((4 - b64.length % 4) % 4);
7119
- const decoded = typeof atob === "function" ? atob(b64 + pad) : Buffer.from(b64 + pad, "base64").toString("utf-8");
7120
- return JSON.parse(decoded);
7121
- } catch {
7122
- return null;
7123
- }
7124
- }
7125
- function tokenToUser(token) {
7126
- const payload = decodeJwtPayload(token);
7127
- if (!payload || typeof payload.sub !== "string") return null;
7128
- if (typeof payload.exp === "number" && payload.exp * 1e3 < Date.now()) {
7129
- return null;
7130
- }
7131
- return {
7132
- uid: payload.sub,
7133
- email: typeof payload.email === "string" ? payload.email : "",
7134
- emailVerified: typeof payload.email_verified === "boolean" ? payload.email_verified : void 0,
7135
- displayName: typeof payload.name === "string" ? payload.name : void 0,
7136
- photoURL: typeof payload.picture === "string" ? payload.picture : void 0,
7137
- isStaff: payload.is_staff === true,
7138
- isCustomer: payload.is_customer === true,
7139
- tenantId: typeof payload.tenant_id === "string" ? payload.tenant_id : void 0,
7140
- ...payload
7141
- };
7142
- }
7143
7171
  function loadCachedUser() {
7144
7172
  if (cachedUser) {
7145
7173
  const exp = cachedUser.exp;
@@ -7160,7 +7188,7 @@ function createOidcAuthNamespace(config) {
7160
7188
  attemptSilentRefresh();
7161
7189
  return null;
7162
7190
  }
7163
- const user = tokenToUser(token);
7191
+ const user = jwtToNeetruUser(token);
7164
7192
  if (!user) {
7165
7193
  storage.removeItem(STORAGE_KEY);
7166
7194
  attemptSilentRefresh();
@@ -7231,7 +7259,7 @@ function createOidcAuthNamespace(config) {
7231
7259
  if (!res.ok || typeof payload?.error === "string") return false;
7232
7260
  const idToken = payload?.id_token;
7233
7261
  if (typeof idToken !== "string" || idToken.length === 0) return false;
7234
- const user = tokenToUser(idToken);
7262
+ const user = jwtToNeetruUser(idToken);
7235
7263
  if (!user) return false;
7236
7264
  const storage = getStorage();
7237
7265
  storage?.setItem(STORAGE_KEY, idToken);
@@ -7277,17 +7305,7 @@ function createOidcAuthNamespace(config) {
7277
7305
  }, msUntilExpiry + 1);
7278
7306
  }
7279
7307
  function resolveClientId() {
7280
- let clientId = config.oidcClientId;
7281
- if (!clientId && config.apiKey) {
7282
- clientId = parseApiKey(config.apiKey).keyId;
7283
- }
7284
- if (!clientId) {
7285
- throw new NeetruError(
7286
- "invalid_config",
7287
- "auth OIDC requer `oidcClientId` (ou `apiKey`) configurado para montar o client_id."
7288
- );
7289
- }
7290
- return clientId;
7308
+ return resolveOidcClientId(config);
7291
7309
  }
7292
7310
  async function handleRedirectCallback(currentUrl) {
7293
7311
  const href = currentUrl ?? (typeof globalThis.location !== "undefined" ? globalThis.location.href : void 0);
@@ -7405,7 +7423,7 @@ function createOidcAuthNamespace(config) {
7405
7423
  "nonce do id_token n\xE3o confere com o da transa\xE7\xE3o OIDC (poss\xEDvel replay). Token descartado."
7406
7424
  );
7407
7425
  }
7408
- const user = tokenToUser(idToken);
7426
+ const user = jwtToNeetruUser(idToken);
7409
7427
  if (!user) {
7410
7428
  throw new NeetruError(
7411
7429
  "token_exchange_failed",
@@ -7450,15 +7468,15 @@ function createOidcAuthNamespace(config) {
7450
7468
  };
7451
7469
  session.setItem(OIDC_TX_KEY, JSON.stringify(tx));
7452
7470
  const idpOrigin = resolveIdpOrigin(config.baseUrl);
7453
- const url = new URL("/api/v1/oauth/authorize", idpOrigin);
7454
- url.searchParams.set("response_type", "code");
7455
- url.searchParams.set("client_id", clientId);
7456
- url.searchParams.set("redirect_uri", redirectUri);
7457
- url.searchParams.set("scope", scope);
7458
- url.searchParams.set("state", state);
7459
- url.searchParams.set("nonce", nonce);
7460
- url.searchParams.set("code_challenge", codeChallenge);
7461
- url.searchParams.set("code_challenge_method", "S256");
7471
+ const url = buildAuthorizeUrl({
7472
+ idpOrigin,
7473
+ clientId,
7474
+ redirectUri,
7475
+ scope,
7476
+ state,
7477
+ nonce,
7478
+ codeChallenge
7479
+ });
7462
7480
  globalThis.location.assign(url.toString());
7463
7481
  return;
7464
7482
  }
@@ -7473,7 +7491,7 @@ function createOidcAuthNamespace(config) {
7473
7491
  const storage = getStorage();
7474
7492
  const token = storage?.getItem(STORAGE_KEY) ?? null;
7475
7493
  if (!token) return null;
7476
- return tokenToUser(token) ? token : null;
7494
+ return jwtToNeetruUser(token) ? token : null;
7477
7495
  },
7478
7496
  async signOut() {
7479
7497
  const storage = getStorage();