@neetru/sdk 3.1.2 → 3.1.6
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 +9 -0
- package/README.md +64 -9
- package/dist/auth.cjs +290 -27
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.mjs +290 -27
- package/dist/auth.mjs.map +1 -1
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.mjs.map +1 -1
- package/dist/checkout.cjs +22 -7
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/checkout.mjs +22 -7
- package/dist/checkout.mjs.map +1 -1
- package/dist/db-react.d.cts +1 -1
- package/dist/db-react.d.ts +1 -1
- package/dist/db.cjs +59 -3
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.mjs +59 -3
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.cjs.map +1 -1
- package/dist/entitlements.d.cts +1 -1
- package/dist/entitlements.d.ts +1 -1
- package/dist/entitlements.mjs.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +11 -6
- package/dist/errors.d.ts +11 -6
- package/dist/errors.mjs.map +1 -1
- package/dist/firestore-compat.d.cts +1 -1
- package/dist/firestore-compat.d.ts +1 -1
- package/dist/index.cjs +302 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.mjs +302 -31
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +51 -7
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +41 -7
- package/dist/mocks.d.ts +41 -7
- package/dist/mocks.mjs +51 -7
- package/dist/mocks.mjs.map +1 -1
- package/dist/notifications.cjs.map +1 -1
- package/dist/notifications.d.cts +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.mjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/support.cjs.map +1 -1
- package/dist/support.d.cts +1 -1
- package/dist/support.d.ts +1 -1
- package/dist/support.mjs.map +1 -1
- package/dist/telemetry.cjs +2 -1
- package/dist/telemetry.cjs.map +1 -1
- package/dist/telemetry.d.cts +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.mjs +2 -1
- package/dist/telemetry.mjs.map +1 -1
- package/dist/{types-DePdSU3P.d.cts → types-BXvGHppn.d.cts} +113 -12
- package/dist/{types-B3XFHD4A.d.ts → types-CQVak2a3.d.ts} +113 -12
- package/dist/usage.cjs.map +1 -1
- package/dist/usage.d.cts +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/usage.mjs.map +1 -1
- package/dist/webhooks.cjs +11 -3
- package/dist/webhooks.cjs.map +1 -1
- package/dist/webhooks.d.cts +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/dist/webhooks.mjs +11 -3
- package/dist/webhooks.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.1.5] - 2026-06-18 — **fix: drift de versão + exemplos quebrados no README**
|
|
9
|
+
|
|
10
|
+
Release **patch**. Item I4 do Conselho de UI (version/reference drift).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`VERSION` exportado** agora bate com o `package.json` (`3.1.5`). Estava pinado em `'3.1.3'` enquanto o pacote publicado já era `3.1.4` → consumers liam a versão errada via `import { VERSION } from '@neetru/sdk'`.
|
|
15
|
+
- **README** parou de mostrar `usage.track()` / `usage.getQuota()` como exemplos vivos — são métodos REMOVIDOS no 3.0 (o próprio README/CHANGELOG documenta a remoção). Trocados por `usage.report(resource, qty)` (devolve `{value, limit, remaining}` inline) e `usage.check(resource)`.
|
|
16
|
+
|
|
8
17
|
## [3.1.0] - 2026-06-05 — **feat: subpath `firestore-compat` + hooks de dados React (frente-B P0)**
|
|
9
18
|
|
|
10
19
|
Release **minor** — só adições, nenhum export removido nem assinatura alterada (a suite de snapshot SDK-QW-5 confirma que a superfície pública 3.0 segue intacta). Upgrade direto de qualquer 3.0.x.
|
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.
|
|
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 {
|
|
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
|
|
107
|
-
|
|
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
|
|
|
@@ -143,10 +194,14 @@ const detailed = await neetru.entitlements.detailed('gestovendas', 'ai_recommend
|
|
|
143
194
|
### Usage tracking
|
|
144
195
|
|
|
145
196
|
```ts
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
// → {
|
|
197
|
+
// Reporta consumo (resource, qty) — devolve {value, limit, remaining} inline,
|
|
198
|
+
// sem precisar de uma chamada de quota separada. Idempotency-Key automático.
|
|
199
|
+
const usage = await neetru.usage.report('reports_generated', 1);
|
|
200
|
+
// → { value: 16, limit: 100, remaining: 84, status: 'ok' }
|
|
201
|
+
|
|
202
|
+
// Verifica entitlement de um resource (sem incrementar o contador).
|
|
203
|
+
const ent = await neetru.usage.check('reports_per_month');
|
|
204
|
+
// → { allowed, limit?, remaining? }
|
|
150
205
|
```
|
|
151
206
|
|
|
152
207
|
### Support
|
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
|
-
|
|
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:
|
|
4607
|
+
return { docs: [], newWatermark: Date.now(), resyncRequired: true };
|
|
4607
4608
|
},
|
|
4608
|
-
async fullResync(
|
|
4609
|
+
async fullResync(collections) {
|
|
4609
4610
|
if (!config) {
|
|
4610
4611
|
return { docs: [], newWatermark: Date.now() };
|
|
4611
4612
|
}
|
|
4612
|
-
|
|
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
|
|
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
|
|
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
|
|
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`);
|
|
@@ -5397,9 +5469,19 @@ var DEV_FIXTURE_USER = Object.freeze({
|
|
|
5397
5469
|
var MockAuth = class {
|
|
5398
5470
|
_user;
|
|
5399
5471
|
_listeners;
|
|
5400
|
-
|
|
5472
|
+
_allowUnsignedDecode;
|
|
5473
|
+
constructor(initialUser = null, options) {
|
|
5401
5474
|
this._user = initialUser;
|
|
5402
5475
|
this._listeners = /* @__PURE__ */ new Set();
|
|
5476
|
+
this._allowUnsignedDecode = options?.allowUnsignedDecode ?? false;
|
|
5477
|
+
if (this._allowUnsignedDecode) {
|
|
5478
|
+
const isTestEnv = typeof process !== "undefined" && (process.env?.NODE_ENV === "test" || process.env?.VITEST === "true" || process.env?.VITEST != null);
|
|
5479
|
+
if (!isTestEnv && typeof console !== "undefined") {
|
|
5480
|
+
console.warn(
|
|
5481
|
+
"[neetru-sdk] MockAuth.verifyToken est\xE1 em modo allowUnsignedDecode=true: tokens N\xC3O t\xEAm assinatura verificada. Este modo \xE9 exclusivo para testes \u2014 NUNCA use como security boundary em c\xF3digo deployado."
|
|
5482
|
+
);
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5403
5485
|
}
|
|
5404
5486
|
async signIn(_options) {
|
|
5405
5487
|
if (!this._user) this._user = { ...DEV_FIXTURE_USER };
|
|
@@ -5424,18 +5506,27 @@ var MockAuth = class {
|
|
|
5424
5506
|
};
|
|
5425
5507
|
}
|
|
5426
5508
|
/**
|
|
5427
|
-
* Mock de `verifyToken` —
|
|
5428
|
-
*
|
|
5429
|
-
* não-vazio sem chamar rede.
|
|
5509
|
+
* Mock de `verifyToken` — por default lança `unauthorized` pra prevenir uso
|
|
5510
|
+
* acidental como security boundary em código deployado.
|
|
5430
5511
|
*
|
|
5431
|
-
* Para
|
|
5512
|
+
* Para decodificar JWTs sem verificar assinatura em testes, crie a instância
|
|
5513
|
+
* com `new MockAuth(user, { allowUnsignedDecode: true })`. Isso é um opt-in
|
|
5514
|
+
* explícito — nunca acontece silenciosamente.
|
|
5515
|
+
*
|
|
5516
|
+
* Para simular falhas ou resultados customizados, use `__mockVerifyToken(fn)`.
|
|
5432
5517
|
*/
|
|
5433
5518
|
async verifyToken(token) {
|
|
5434
5519
|
if (this._verifyTokenOverride) {
|
|
5435
5520
|
return this._verifyTokenOverride(token);
|
|
5436
5521
|
}
|
|
5437
5522
|
if (!token) {
|
|
5438
|
-
throw
|
|
5523
|
+
throw new NeetruError("token_invalid_signature", "Token inv\xE1lido: string vazia");
|
|
5524
|
+
}
|
|
5525
|
+
if (!this._allowUnsignedDecode) {
|
|
5526
|
+
throw new NeetruError(
|
|
5527
|
+
"unauthorized",
|
|
5528
|
+
"MockAuth.verifyToken n\xE3o verifica assinaturas JWT e N\xC3O deve ser usado como security boundary em c\xF3digo deployado. Em testes, use new MockAuth(user, { allowUnsignedDecode: true }) para habilitar decodifica\xE7\xE3o sem assinatura, ou __mockVerifyToken(fn) para injetar resultado customizado."
|
|
5529
|
+
);
|
|
5439
5530
|
}
|
|
5440
5531
|
try {
|
|
5441
5532
|
const parts = token.split(".");
|
|
@@ -5462,7 +5553,32 @@ var MockAuth = class {
|
|
|
5462
5553
|
} catch {
|
|
5463
5554
|
}
|
|
5464
5555
|
if (this._user) return this._user;
|
|
5465
|
-
throw
|
|
5556
|
+
throw new NeetruError("token_invalid_signature", "Nenhum user mock dispon\xEDvel e JWT sem sub v\xE1lido");
|
|
5557
|
+
}
|
|
5558
|
+
/**
|
|
5559
|
+
* Mock de `handleRedirectCallback` (Bug 2 / 3.1.3) — não há redirect real no
|
|
5560
|
+
* mock, então retorna `null` por default. Use `__setPendingCallback(result)`
|
|
5561
|
+
* em testes pra simular um retorno do IdP.
|
|
5562
|
+
*/
|
|
5563
|
+
async handleRedirectCallback(_currentUrl) {
|
|
5564
|
+
return this._pendingCallback;
|
|
5565
|
+
}
|
|
5566
|
+
/**
|
|
5567
|
+
* Mock de `getIdToken` (Bug 2 / 3.1.3). Retorna `null` por default; use
|
|
5568
|
+
* `__setIdToken(token)` pra injetar um token cru em testes.
|
|
5569
|
+
*/
|
|
5570
|
+
getIdToken() {
|
|
5571
|
+
return this._idToken;
|
|
5572
|
+
}
|
|
5573
|
+
_pendingCallback = null;
|
|
5574
|
+
_idToken = null;
|
|
5575
|
+
/** Test helper — simula o material devolvido por um callback OIDC. */
|
|
5576
|
+
__setPendingCallback(result) {
|
|
5577
|
+
this._pendingCallback = result;
|
|
5578
|
+
}
|
|
5579
|
+
/** Test helper — injeta o id_token cru retornado por `getIdToken`. */
|
|
5580
|
+
__setIdToken(token) {
|
|
5581
|
+
this._idToken = token;
|
|
5466
5582
|
}
|
|
5467
5583
|
_verifyTokenOverride = null;
|
|
5468
5584
|
/**
|
|
@@ -5682,6 +5798,49 @@ function _jwtPayloadToUser(payload) {
|
|
|
5682
5798
|
...payload
|
|
5683
5799
|
};
|
|
5684
5800
|
}
|
|
5801
|
+
var OIDC_TX_KEY = "neetru_oauth_tx";
|
|
5802
|
+
function resolveIdpOrigin(baseUrl) {
|
|
5803
|
+
const overrideAuthUrl = typeof globalThis.NEETRU_AUTH_URL === "string" ? globalThis.NEETRU_AUTH_URL : null;
|
|
5804
|
+
return overrideAuthUrl ?? baseUrl.replace(/^https?:\/\/api\./, "https://auth.");
|
|
5805
|
+
}
|
|
5806
|
+
function getWebCrypto() {
|
|
5807
|
+
const c = globalThis.crypto;
|
|
5808
|
+
if (!c || typeof c.getRandomValues !== "function" || !c.subtle) {
|
|
5809
|
+
throw new NeetruError(
|
|
5810
|
+
"runtime_unsupported",
|
|
5811
|
+
"auth.signIn requer WebCrypto (crypto.subtle) \u2014 indispon\xEDvel neste runtime. Use HTTPS (ou http://localhost) num browser moderno."
|
|
5812
|
+
);
|
|
5813
|
+
}
|
|
5814
|
+
return c;
|
|
5815
|
+
}
|
|
5816
|
+
function base64UrlFromBytes(bytes) {
|
|
5817
|
+
let str = "";
|
|
5818
|
+
for (const b of bytes) str += String.fromCharCode(b);
|
|
5819
|
+
const b64 = typeof btoa === "function" ? btoa(str) : Buffer.from(str, "binary").toString("base64");
|
|
5820
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
5821
|
+
}
|
|
5822
|
+
function randomUrlSafe(crypto2, byteLen) {
|
|
5823
|
+
const bytes = new Uint8Array(byteLen);
|
|
5824
|
+
crypto2.getRandomValues(bytes);
|
|
5825
|
+
return base64UrlFromBytes(bytes);
|
|
5826
|
+
}
|
|
5827
|
+
async function pkceChallengeS256(crypto2, verifier) {
|
|
5828
|
+
const data = new TextEncoder().encode(verifier);
|
|
5829
|
+
const digest = await crypto2.subtle.digest("SHA-256", data);
|
|
5830
|
+
return base64UrlFromBytes(new Uint8Array(digest));
|
|
5831
|
+
}
|
|
5832
|
+
function cleanCallbackUrl(parsed) {
|
|
5833
|
+
try {
|
|
5834
|
+
const history = globalThis.history;
|
|
5835
|
+
if (!history || typeof history.replaceState !== "function") return;
|
|
5836
|
+
const clean = new URL(parsed.toString());
|
|
5837
|
+
for (const p of ["code", "state", "error", "error_description", "iss", "session_state"]) {
|
|
5838
|
+
clean.searchParams.delete(p);
|
|
5839
|
+
}
|
|
5840
|
+
history.replaceState(history.state ?? null, "", clean.pathname + clean.search + clean.hash);
|
|
5841
|
+
} catch {
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5685
5844
|
function createOidcAuthNamespace(config) {
|
|
5686
5845
|
const STORAGE_KEY = "neetru_id_token";
|
|
5687
5846
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -5694,6 +5853,13 @@ function createOidcAuthNamespace(config) {
|
|
|
5694
5853
|
return null;
|
|
5695
5854
|
}
|
|
5696
5855
|
}
|
|
5856
|
+
function getSessionStorage() {
|
|
5857
|
+
try {
|
|
5858
|
+
return typeof globalThis.sessionStorage !== "undefined" ? globalThis.sessionStorage : null;
|
|
5859
|
+
} catch {
|
|
5860
|
+
return null;
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5697
5863
|
function decodeJwtPayload(token) {
|
|
5698
5864
|
const parts = token.split(".");
|
|
5699
5865
|
if (parts.length !== 3) return null;
|
|
@@ -5777,20 +5943,48 @@ function createOidcAuthNamespace(config) {
|
|
|
5777
5943
|
return {
|
|
5778
5944
|
async signIn(options) {
|
|
5779
5945
|
if (typeof globalThis.location !== "undefined" && typeof globalThis.location.assign === "function") {
|
|
5946
|
+
const crypto2 = getWebCrypto();
|
|
5780
5947
|
const redirectUri = options?.redirectUri ?? globalThis.location.origin;
|
|
5781
5948
|
const scope = options?.scope ?? "openid profile email";
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5949
|
+
let clientId = config.oidcClientId;
|
|
5950
|
+
if (!clientId && config.apiKey) {
|
|
5951
|
+
clientId = parseApiKey(config.apiKey).keyId;
|
|
5952
|
+
}
|
|
5953
|
+
if (!clientId) {
|
|
5954
|
+
throw new NeetruError(
|
|
5955
|
+
"invalid_config",
|
|
5956
|
+
"auth.signIn requer `oidcClientId` (ou `apiKey`) configurado para montar o client_id."
|
|
5957
|
+
);
|
|
5958
|
+
}
|
|
5959
|
+
const state = randomUrlSafe(crypto2, 16);
|
|
5960
|
+
const nonce = randomUrlSafe(crypto2, 16);
|
|
5961
|
+
const codeVerifier = randomUrlSafe(crypto2, 32);
|
|
5962
|
+
const codeChallenge = await pkceChallengeS256(crypto2, codeVerifier);
|
|
5963
|
+
const session = getSessionStorage();
|
|
5964
|
+
if (!session) {
|
|
5965
|
+
throw new NeetruError(
|
|
5966
|
+
"runtime_unsupported",
|
|
5967
|
+
"auth.signIn requer sessionStorage para guardar a transa\xE7\xE3o PKCE entre o redirect e o callback."
|
|
5968
|
+
);
|
|
5969
|
+
}
|
|
5970
|
+
const tx = {
|
|
5971
|
+
state,
|
|
5972
|
+
nonce,
|
|
5973
|
+
codeVerifier,
|
|
5974
|
+
redirectUri,
|
|
5975
|
+
postLoginRedirect: options?.postLoginRedirect
|
|
5976
|
+
};
|
|
5977
|
+
session.setItem(OIDC_TX_KEY, JSON.stringify(tx));
|
|
5978
|
+
const idpOrigin = resolveIdpOrigin(config.baseUrl);
|
|
5785
5979
|
const url = new URL("/api/v1/oauth/authorize", idpOrigin);
|
|
5786
5980
|
url.searchParams.set("response_type", "code");
|
|
5981
|
+
url.searchParams.set("client_id", clientId);
|
|
5787
5982
|
url.searchParams.set("redirect_uri", redirectUri);
|
|
5788
5983
|
url.searchParams.set("scope", scope);
|
|
5789
5984
|
url.searchParams.set("state", state);
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
}
|
|
5985
|
+
url.searchParams.set("nonce", nonce);
|
|
5986
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
5987
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
5794
5988
|
globalThis.location.assign(url.toString());
|
|
5795
5989
|
return;
|
|
5796
5990
|
}
|
|
@@ -5799,6 +5993,67 @@ function createOidcAuthNamespace(config) {
|
|
|
5799
5993
|
"auth.signIn requires a browser context or mocks. Use NEETRU_ENV=dev or pass mocks.auth."
|
|
5800
5994
|
);
|
|
5801
5995
|
},
|
|
5996
|
+
async handleRedirectCallback(currentUrl) {
|
|
5997
|
+
const href = currentUrl ?? (typeof globalThis.location !== "undefined" ? globalThis.location.href : void 0);
|
|
5998
|
+
if (!href) return null;
|
|
5999
|
+
let parsed;
|
|
6000
|
+
try {
|
|
6001
|
+
parsed = new URL(href);
|
|
6002
|
+
} catch {
|
|
6003
|
+
return null;
|
|
6004
|
+
}
|
|
6005
|
+
const params = parsed.searchParams;
|
|
6006
|
+
const idpError = params.get("error");
|
|
6007
|
+
if (idpError) {
|
|
6008
|
+
const desc = params.get("error_description") ?? idpError;
|
|
6009
|
+
getSessionStorage()?.removeItem(OIDC_TX_KEY);
|
|
6010
|
+
cleanCallbackUrl(parsed);
|
|
6011
|
+
throw new NeetruError(idpError, `OIDC authorize falhou: ${desc}`);
|
|
6012
|
+
}
|
|
6013
|
+
const code = params.get("code");
|
|
6014
|
+
if (!code) return null;
|
|
6015
|
+
const returnedState = params.get("state");
|
|
6016
|
+
const session = getSessionStorage();
|
|
6017
|
+
const txRaw = session?.getItem(OIDC_TX_KEY) ?? null;
|
|
6018
|
+
let tx = null;
|
|
6019
|
+
if (txRaw) {
|
|
6020
|
+
try {
|
|
6021
|
+
tx = JSON.parse(txRaw);
|
|
6022
|
+
} catch {
|
|
6023
|
+
tx = null;
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
if (!tx) {
|
|
6027
|
+
throw new NeetruError(
|
|
6028
|
+
"oidc_state_mismatch",
|
|
6029
|
+
"Callback OIDC sem transa\xE7\xE3o pendente \u2014 `signIn` n\xE3o foi iniciado nesta sess\xE3o (ou j\xE1 consumido)."
|
|
6030
|
+
);
|
|
6031
|
+
}
|
|
6032
|
+
if (!returnedState || returnedState !== tx.state) {
|
|
6033
|
+
session?.removeItem(OIDC_TX_KEY);
|
|
6034
|
+
cleanCallbackUrl(parsed);
|
|
6035
|
+
throw new NeetruError(
|
|
6036
|
+
"oidc_state_mismatch",
|
|
6037
|
+
"state do callback OIDC n\xE3o confere com o da transa\xE7\xE3o (poss\xEDvel CSRF)."
|
|
6038
|
+
);
|
|
6039
|
+
}
|
|
6040
|
+
session?.removeItem(OIDC_TX_KEY);
|
|
6041
|
+
cleanCallbackUrl(parsed);
|
|
6042
|
+
return {
|
|
6043
|
+
code,
|
|
6044
|
+
codeVerifier: tx.codeVerifier,
|
|
6045
|
+
redirectUri: tx.redirectUri,
|
|
6046
|
+
nonce: tx.nonce,
|
|
6047
|
+
state: tx.state,
|
|
6048
|
+
postLoginRedirect: tx.postLoginRedirect
|
|
6049
|
+
};
|
|
6050
|
+
},
|
|
6051
|
+
getIdToken() {
|
|
6052
|
+
const storage = getStorage();
|
|
6053
|
+
const token = storage?.getItem(STORAGE_KEY) ?? null;
|
|
6054
|
+
if (!token) return null;
|
|
6055
|
+
return tokenToUser(token) ? token : null;
|
|
6056
|
+
},
|
|
5802
6057
|
async signOut() {
|
|
5803
6058
|
const storage = getStorage();
|
|
5804
6059
|
const storedToken = storage?.getItem(STORAGE_KEY) ?? null;
|
|
@@ -5858,13 +6113,20 @@ function createOidcAuthNamespace(config) {
|
|
|
5858
6113
|
const issuer = idpOrigin;
|
|
5859
6114
|
const jwksUrl = `${idpOrigin}/api/v1/oauth/.well-known/jwks.json`;
|
|
5860
6115
|
const cacheTtlMs = options?.jwksCacheTtlMs ?? DEFAULT_JWKS_TTL_MS;
|
|
6116
|
+
const audienceFromOptions = options?.audience;
|
|
5861
6117
|
const parsedKey = tryParseApiKey(config.apiKey);
|
|
5862
|
-
const audience = parsedKey?.keyId;
|
|
6118
|
+
const audience = audienceFromOptions ?? parsedKey?.keyId;
|
|
6119
|
+
if (!audience) {
|
|
6120
|
+
throw new NeetruError(
|
|
6121
|
+
"invalid_config",
|
|
6122
|
+
"verifyToken requires an audience: provide apiKey on the client or pass options.audience. Calling jwtVerify without an audience allows tokens issued for other OIDC clients to pass."
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
5863
6125
|
const jwks = _getJwks(jwksUrl, cacheTtlMs);
|
|
5864
6126
|
try {
|
|
5865
6127
|
const { payload } = await jose.jwtVerify(token, jwks, {
|
|
5866
6128
|
issuer,
|
|
5867
|
-
|
|
6129
|
+
audience,
|
|
5868
6130
|
algorithms: ["RS256"]
|
|
5869
6131
|
});
|
|
5870
6132
|
return _jwtPayloadToUser(payload);
|
|
@@ -5905,6 +6167,7 @@ function createNeetruClient(config = {}) {
|
|
|
5905
6167
|
const env = resolveEnv(config.env);
|
|
5906
6168
|
const resolved = Object.freeze({
|
|
5907
6169
|
apiKey,
|
|
6170
|
+
oidcClientId: config.oidcClientId,
|
|
5908
6171
|
baseUrl,
|
|
5909
6172
|
fetch: fetchImpl.bind(globalThis),
|
|
5910
6173
|
env,
|