@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.
- package/README.md +56 -5
- package/dist/auth.cjs +255 -18
- 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 +255 -18
- 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.cjs +15 -2
- package/dist/firestore-compat.cjs.map +1 -1
- package/dist/firestore-compat.d.cts +1 -1
- package/dist/firestore-compat.d.ts +1 -1
- package/dist/firestore-compat.mjs +15 -2
- package/dist/firestore-compat.mjs.map +1 -1
- package/dist/index.cjs +267 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +267 -22
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.cjs +25 -0
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.cts +18 -1
- package/dist/mocks.d.ts +18 -1
- package/dist/mocks.mjs +25 -0
- 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.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.mjs.map +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-B3XFHD4A.d.ts → types-CRIIjqzC.d.ts} +105 -12
- package/dist/{types-DePdSU3P.d.cts → types-DvMdYbfV.d.cts} +105 -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/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
|
|
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`);
|
|
@@ -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
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
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
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
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,
|