@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 +22 -1
- package/dist/ai.cjs +1 -1
- package/dist/ai.mjs +1 -1
- package/dist/auth.cjs +103 -85
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.mjs +103 -85
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.cjs +1 -1
- package/dist/catalog.mjs +1 -1
- package/dist/checkout.cjs +1 -1
- package/dist/checkout.mjs +1 -1
- package/dist/db.cjs +1 -1
- package/dist/db.mjs +1 -1
- package/dist/index.cjs +104 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +104 -86
- package/dist/index.mjs.map +1 -1
- package/dist/mobile.cjs +515 -0
- package/dist/mobile.cjs.map +1 -0
- package/dist/mobile.d.cts +89 -0
- package/dist/mobile.d.ts +89 -0
- package/dist/mobile.mjs +513 -0
- package/dist/mobile.mjs.map +1 -0
- package/dist/notifications.cjs +1 -1
- package/dist/notifications.mjs +1 -1
- package/dist/support.cjs +1 -1
- package/dist/support.mjs +1 -1
- package/dist/telemetry.cjs +1 -1
- package/dist/telemetry.mjs +1 -1
- package/dist/usage.cjs +1 -1
- package/dist/usage.mjs +1 -1
- package/dist/webhooks.cjs +1 -1
- package/dist/webhooks.mjs +1 -1
- package/package.json +6 -1
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
package/dist/ai.mjs
CHANGED
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
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
|
|
7494
|
+
return jwtToNeetruUser(token) ? token : null;
|
|
7477
7495
|
},
|
|
7478
7496
|
async signOut() {
|
|
7479
7497
|
const storage = getStorage();
|