@mybuildex/pkg 0.3.4 → 0.4.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/README.md +23 -0
- package/dist/http/public-route.d.ts +33 -0
- package/dist/http/public-route.js +91 -0
- package/dist/http/public-route.js.map +1 -0
- package/dist/http/shared-secret.d.ts +14 -0
- package/dist/http/shared-secret.js +37 -0
- package/dist/http/shared-secret.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/scope/request-scope.d.ts +28 -0
- package/dist/scope/request-scope.js +123 -0
- package/dist/scope/request-scope.js.map +1 -0
- package/dist/tenant/auth.d.ts +5 -2
- package/dist/tenant/auth.js +2 -1
- package/dist/tenant/auth.js.map +1 -1
- package/dist/tenant/membership.d.ts +21 -0
- package/dist/tenant/membership.js +78 -0
- package/dist/tenant/membership.js.map +1 -0
- package/dist/tenant/middleware.d.ts +7 -0
- package/dist/tenant/middleware.js +29 -0
- package/dist/tenant/middleware.js.map +1 -1
- package/dist/tenant/resolve.d.ts +3 -0
- package/dist/tenant/resolve.js +8 -2
- package/dist/tenant/resolve.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,29 @@ const mt = createMultitenantPrisma({
|
|
|
26
26
|
|
|
27
27
|
A lib **não** depende de `@prisma/client`/`mariadb`: cada API injeta o client gerado e o driver nas versões dela.
|
|
28
28
|
|
|
29
|
+
## 0.4.0 — concorrência e isolamento entre escolas
|
|
30
|
+
|
|
31
|
+
Quatro módulos novos, todos opt-in (nada muda numa API que não os usar):
|
|
32
|
+
|
|
33
|
+
- `createRequestScopePlugin()` + `requestScoped(Ctor, ...args)` — escopo por request via `AsyncLocalStorage`. Resolve o padrão `const service = new XService(fastify)` + `service.setRequest(request)` no preHandler, que compartilhava `this.request` (e o `request.prisma` do tenant) entre requests concorrentes. Troca mecânica: `new XService(fastify)` → `requestScoped(XService, fastify)`; registrar o plugin antes dos controllers. Uso fora de request lança `RequestScopeError`.
|
|
34
|
+
- `createTenantMembershipAssert({getTenantClient})` — o `assertTenant` pronto: consulta `users.firebase_id = uid` no schema do `:id_school` da URL; sem vínculo → 403. Fail-closed em erro de banco; cache positivo por `escola:uid` (120 s).
|
|
35
|
+
- `createTenantMiddleware({..., strictTenant: true})` — `id_school` presente mas inválido responde 400 (`InvalidTenantError`) em vez de cair no client default, que é a escola do `DATABASE_URL`. Sem `id_school` nenhum, o default continua.
|
|
36
|
+
- `createSharedSecretGuard({env: "INTERNAL_API_SECRET", header: "x-internal-secret"})` — `preHandler` para rotas chamadas entre APIs (ex: `emailsend`): compara o segredo do env em tempo constante; 401 se inválido, 503 (fail-closed) se a variável não existe em produção.
|
|
37
|
+
- `publicRoute(recurso, sufixo, {methods, prefix})`, `publicPath`, `isPublicRoute`, `safeEqual` — rotas públicas ancoradas (`^/v1/\d+/recurso/sufixo$`, com filtro de método) no lugar de regex de substring, e comparação de segredo/HMAC em tempo constante. `createAuthMiddleware.publicRoutes` aceita as regras e continua aceitando `RegExp`.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import {createRequestScopePlugin, requestScoped, createTenantMembershipAssert, publicRoute} from "@mybuildex/pkg";
|
|
41
|
+
|
|
42
|
+
await createRequestScopePlugin()(fastify); // antes do autoload dos módulos
|
|
43
|
+
const service = requestScoped(PayService, fastify); // em cada controller
|
|
44
|
+
createAuthMiddleware({
|
|
45
|
+
publicRoutes: [publicRoute("pay_checkouts", "public/new", {methods: ["POST"]})],
|
|
46
|
+
assertTenant: createTenantMembershipAssert({getTenantClient}),
|
|
47
|
+
...
|
|
48
|
+
});
|
|
49
|
+
createTenantMiddleware({getTenantClient, defaultClient, strictTenant: true});
|
|
50
|
+
```
|
|
51
|
+
|
|
29
52
|
## Consumo pelas APIs (npmjs público)
|
|
30
53
|
|
|
31
54
|
O pacote é publicado **público no npmjs** — só o `dist/` compilado vai no tarball (`files: ["dist"]`); testes e repo continuam privados. Zero token: `npm i` funciona no dev, no Actions e no build do GCP.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface PublicRouteOptions {
|
|
2
|
+
/** Prefixo opcional antes de /v1 (ex: "v2_payments" quando a função recebe
|
|
3
|
+
* o path completo). Aceito como opcional na regex: casa com e sem ele. */
|
|
4
|
+
prefix?: string;
|
|
5
|
+
/** Versão da rota (default "v1") */
|
|
6
|
+
version?: string;
|
|
7
|
+
/** Métodos HTTP liberados (default: todos). Comparação case-insensitive. */
|
|
8
|
+
methods?: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface PublicRouteRule {
|
|
11
|
+
pattern: RegExp;
|
|
12
|
+
methods?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Regra de rota pública ancorada: `^(/prefix)?/v1/\d+/recurso(/sufixo)?$`.
|
|
16
|
+
* Exemplos:
|
|
17
|
+
* publicRoute("pay_checkouts", "public/new") -> POST /v1/3/pay_checkouts/public/new
|
|
18
|
+
* publicRoute("pay", "public/:hash") -> /v1/3/pay/public/eyJ...
|
|
19
|
+
* publicRoute("pay_products", ":id", {methods: ["GET"]}) -> só GET /v1/3/pay_products/7
|
|
20
|
+
*/
|
|
21
|
+
export declare function publicRoute(resource: string, suffix?: string, options?: PublicRouteOptions): PublicRouteRule;
|
|
22
|
+
/** Rota pública fora do padrão /v1/:id_school (ex: documentação): regex ancorada literal. */
|
|
23
|
+
export declare function publicPath(pathname: string, options?: {
|
|
24
|
+
methods?: string[];
|
|
25
|
+
}): PublicRouteRule;
|
|
26
|
+
export type PublicRouteLike = RegExp | PublicRouteRule;
|
|
27
|
+
/** Um pathname (sem query string) + método casam com alguma regra? */
|
|
28
|
+
export declare function isPublicRoute(rules: PublicRouteLike[], pathname: string, method?: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Comparação em tempo constante. Devolve false (nunca lança) quando qualquer
|
|
31
|
+
* lado não é string ou quando os tamanhos diferem.
|
|
32
|
+
*/
|
|
33
|
+
export declare function safeEqual(a: unknown, b: unknown): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Rotas públicas ANCORADAS e comparação segura de segredos.
|
|
3
|
+
//
|
|
4
|
+
// As APIs liberavam rotas públicas com regex de substring
|
|
5
|
+
// (`/\/public\b/`, `/\/pay_products\/\b/`): qualquer path que contivesse o
|
|
6
|
+
// trecho passava sem token — inclusive `/pay_products/delete/5`. `publicRoute`
|
|
7
|
+
// gera uma regex ancorada no formato real das rotas (`/v1/:id_school/recurso/
|
|
8
|
+
// sufixo`), opcionalmente restrita a métodos HTTP.
|
|
9
|
+
//
|
|
10
|
+
// `safeEqual` encapsula `crypto.timingSafeEqual` para comparar assinaturas de
|
|
11
|
+
// webhook e segredos sem vazar o ponto de divergência pelo tempo de resposta,
|
|
12
|
+
// e sem lançar quando os tamanhos diferem.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.publicRoute = publicRoute;
|
|
15
|
+
exports.publicPath = publicPath;
|
|
16
|
+
exports.isPublicRoute = isPublicRoute;
|
|
17
|
+
exports.safeEqual = safeEqual;
|
|
18
|
+
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
function escapeSegment(segment) {
|
|
20
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
}
|
|
22
|
+
/** Converte "public/:hash" em "public/[^/]+" (cada :param vira um segmento).
|
|
23
|
+
* Um param pode restringir o formato: ":id(\\d+)" vira "(?:\\d+)". */
|
|
24
|
+
function suffixToPattern(suffix) {
|
|
25
|
+
if (!suffix)
|
|
26
|
+
return "";
|
|
27
|
+
return "/" + suffix
|
|
28
|
+
.replace(/^\/+|\/+$/g, "")
|
|
29
|
+
.split("/")
|
|
30
|
+
.map(part => {
|
|
31
|
+
const param = /^:[A-Za-z_][A-Za-z0-9_]*(?:\((.+)\))?$/.exec(part);
|
|
32
|
+
if (!param)
|
|
33
|
+
return escapeSegment(part);
|
|
34
|
+
return param[1] ? `(?:${param[1]})` : "[^/]+";
|
|
35
|
+
})
|
|
36
|
+
.join("/");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Regra de rota pública ancorada: `^(/prefix)?/v1/\d+/recurso(/sufixo)?$`.
|
|
40
|
+
* Exemplos:
|
|
41
|
+
* publicRoute("pay_checkouts", "public/new") -> POST /v1/3/pay_checkouts/public/new
|
|
42
|
+
* publicRoute("pay", "public/:hash") -> /v1/3/pay/public/eyJ...
|
|
43
|
+
* publicRoute("pay_products", ":id", {methods: ["GET"]}) -> só GET /v1/3/pay_products/7
|
|
44
|
+
*/
|
|
45
|
+
function publicRoute(resource, suffix = "", options = {}) {
|
|
46
|
+
const version = escapeSegment(options.version ?? "v1");
|
|
47
|
+
const prefix = options.prefix ? `(?:/${escapeSegment(options.prefix.replace(/^\/+|\/+$/g, ""))})?` : "";
|
|
48
|
+
const source = `^${prefix}/${version}/\\d+/${escapeSegment(resource)}${suffixToPattern(suffix)}$`;
|
|
49
|
+
const rule = { pattern: new RegExp(source) };
|
|
50
|
+
if (options.methods && options.methods.length) {
|
|
51
|
+
rule.methods = options.methods.map(m => m.toUpperCase());
|
|
52
|
+
}
|
|
53
|
+
return rule;
|
|
54
|
+
}
|
|
55
|
+
/** Rota pública fora do padrão /v1/:id_school (ex: documentação): regex ancorada literal. */
|
|
56
|
+
function publicPath(pathname, options = {}) {
|
|
57
|
+
const rule = { pattern: new RegExp(`^${escapeSegment(pathname)}$`) };
|
|
58
|
+
if (options.methods && options.methods.length)
|
|
59
|
+
rule.methods = options.methods.map(m => m.toUpperCase());
|
|
60
|
+
return rule;
|
|
61
|
+
}
|
|
62
|
+
/** Um pathname (sem query string) + método casam com alguma regra? */
|
|
63
|
+
function isPublicRoute(rules, pathname, method = "GET") {
|
|
64
|
+
const upper = String(method || "GET").toUpperCase();
|
|
65
|
+
for (const rule of rules) {
|
|
66
|
+
if (rule instanceof RegExp) {
|
|
67
|
+
if (rule.test(pathname))
|
|
68
|
+
return true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!rule.pattern.test(pathname))
|
|
72
|
+
continue;
|
|
73
|
+
if (!rule.methods || rule.methods.includes(upper))
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Comparação em tempo constante. Devolve false (nunca lança) quando qualquer
|
|
80
|
+
* lado não é string ou quando os tamanhos diferem.
|
|
81
|
+
*/
|
|
82
|
+
function safeEqual(a, b) {
|
|
83
|
+
if (typeof a !== "string" || typeof b !== "string")
|
|
84
|
+
return false;
|
|
85
|
+
const bufA = Buffer.from(a, "utf8");
|
|
86
|
+
const bufB = Buffer.from(b, "utf8");
|
|
87
|
+
if (bufA.length !== bufB.length)
|
|
88
|
+
return false;
|
|
89
|
+
return (0, node_crypto_1.timingSafeEqual)(bufA, bufB);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=public-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-route.js","sourceRoot":"","sources":["../../src/http/public-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SharedSecretGuardOptions {
|
|
2
|
+
/** Nome da variável de ambiente com o segredo (default INTERNAL_API_SECRET) */
|
|
3
|
+
env?: string;
|
|
4
|
+
/** Header lido no request (default x-internal-secret) */
|
|
5
|
+
header?: string;
|
|
6
|
+
/** Aceitar o segredo também por query string com esse nome (default: não) */
|
|
7
|
+
query?: string;
|
|
8
|
+
/** Sem a variável e fora de produção: liberar (default false) */
|
|
9
|
+
allowMissingInDev?: boolean;
|
|
10
|
+
/** Considerado produção quando true (default: NODE_ENV === "production") */
|
|
11
|
+
isProduction?: () => boolean;
|
|
12
|
+
log?: (message: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createSharedSecretGuard(options?: SharedSecretGuardOptions): (request: any, reply: any) => Promise<any>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Guard de segredo compartilhado para chamadas ENTRE APIs (ex: v2_payments ->
|
|
3
|
+
// v1_atendimento/emailsend). O valor vem de env (Secret Manager, o mesmo nas
|
|
4
|
+
// duas functions); a comparação é em tempo constante (safeEqual).
|
|
5
|
+
//
|
|
6
|
+
// Fail-closed: em produção, sem a variável configurada a rota responde 503 e
|
|
7
|
+
// loga — nunca fica aberta por omissão. Fora de produção, `allowMissingInDev`
|
|
8
|
+
// libera explicitamente (nunca por hostname).
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.createSharedSecretGuard = createSharedSecretGuard;
|
|
11
|
+
const public_route_1 = require("./public-route");
|
|
12
|
+
function createSharedSecretGuard(options = {}) {
|
|
13
|
+
const ENV = options.env ?? "INTERNAL_API_SECRET";
|
|
14
|
+
const HEADER = (options.header ?? "x-internal-secret").toLowerCase();
|
|
15
|
+
const QUERY = options.query;
|
|
16
|
+
const isProduction = options.isProduction ?? (() => process.env.NODE_ENV === "production");
|
|
17
|
+
const log = options.log ?? ((m) => console.error(m));
|
|
18
|
+
/** preHandler/onRequest do Fastify: `{preHandler: guard}` na rota interna */
|
|
19
|
+
return async function sharedSecretGuard(request, reply) {
|
|
20
|
+
const expected = process.env[ENV];
|
|
21
|
+
if (!expected) {
|
|
22
|
+
if (!isProduction() && options.allowMissingInDev)
|
|
23
|
+
return;
|
|
24
|
+
log(`[shared-secret] variável ${ENV} ausente: rota interna indisponível`);
|
|
25
|
+
return reply.status(503).send({ statusCode: 503, message: "Rota interna indisponível: segredo não configurado" });
|
|
26
|
+
}
|
|
27
|
+
let provided = request.headers?.[HEADER];
|
|
28
|
+
if (provided === undefined && QUERY)
|
|
29
|
+
provided = request.query?.[QUERY];
|
|
30
|
+
if (Array.isArray(provided))
|
|
31
|
+
provided = provided[0];
|
|
32
|
+
if (!(0, public_route_1.safeEqual)(provided, expected)) {
|
|
33
|
+
return reply.status(401).send({ statusCode: 401, message: "Segredo interno inválido" });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=shared-secret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-secret.js","sourceRoot":"","sources":["../../src/http/shared-secret.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,kEAAkE;AAClE,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,8CAA8C;;AAkB9C,0DAwBC;AAxCD,iDAAyC;AAgBzC,SAAgB,uBAAuB,CAAC,UAAoC,EAAE;IAC1E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,qBAAqB,CAAC;IACjD,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,mBAAmB,CAAC,CAAC,WAAW,EAAE,CAAC;IACrE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC5B,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC;IAC3F,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7D,6EAA6E;IAC7E,OAAO,KAAK,UAAU,iBAAiB,CAAC,OAAY,EAAE,KAAU;QAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,IAAI,CAAC,YAAY,EAAE,IAAI,OAAO,CAAC,iBAAiB;gBAAE,OAAO;YACzD,GAAG,CAAC,4BAA4B,GAAG,qCAAqC,CAAC,CAAC;YAC1E,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAC,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,oDAAoD,EAAC,CAAC,CAAC;QACpH,CAAC;QAED,IAAI,QAAQ,GAAY,OAAO,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,QAAQ,KAAK,SAAS,IAAI,KAAK;YAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC;QACvE,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAEpD,IAAI,CAAC,IAAA,wBAAS,EAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YACjC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAC,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,0BAA0B,EAAC,CAAC,CAAC;QAC1F,CAAC;IACL,CAAC,CAAC;AACN,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
export { resolveTenantId } from "./tenant/resolve";
|
|
1
|
+
export { resolveTenantId, readRawTenantId } from "./tenant/resolve";
|
|
2
2
|
export type { TenantRequestLike } from "./tenant/resolve";
|
|
3
|
-
export { createTenantMiddleware } from "./tenant/middleware";
|
|
3
|
+
export { createTenantMiddleware, InvalidTenantError } from "./tenant/middleware";
|
|
4
4
|
export type { TenantMiddlewareOptions } from "./tenant/middleware";
|
|
5
5
|
export { createAuthMiddleware } from "./tenant/auth";
|
|
6
6
|
export type { AuthMiddlewareOptions, TenantAuthLike } from "./tenant/auth";
|
|
7
|
+
export { createTenantMembershipAssert } from "./tenant/membership";
|
|
8
|
+
export type { TenantMembershipOptions, TenantMembershipAssert } from "./tenant/membership";
|
|
9
|
+
export { createRequestScopePlugin, requestScoped, getRequestScope, runInRequestScope, RequestScopeError } from "./scope/request-scope";
|
|
10
|
+
export type { RequestScopeStore } from "./scope/request-scope";
|
|
11
|
+
export { publicRoute, publicPath, isPublicRoute, safeEqual } from "./http/public-route";
|
|
12
|
+
export { createSharedSecretGuard } from "./http/shared-secret";
|
|
13
|
+
export type { SharedSecretGuardOptions } from "./http/shared-secret";
|
|
14
|
+
export type { PublicRouteOptions, PublicRouteRule, PublicRouteLike } from "./http/public-route";
|
|
7
15
|
export { createMultitenantPrisma } from "./prisma/multitenant";
|
|
8
16
|
export { createMultitenantPrismaMariaDb } from "./prisma/mariadb-factory";
|
|
9
17
|
export type { MariaDbMultitenantOptions } from "./prisma/mariadb-factory";
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EMBED_MODEL = exports.GEMINI_MODEL = exports.EMBED_DIM = exports.GeminiHelp = exports.createGcfHandler = exports.isInternalError = exports.createErrorHandler = exports.getBucket = exports.getDefaultAuth = exports.getTenantAuth = exports.initFirebaseAdmin = exports.getFirebaseStorage = exports.getFirebaseClientApp = exports.createFirebaseClientPlugin = exports.createSwaggerPlugin = exports.DEFAULT_CORS = exports.createCorsPlugin = exports.withTenantSchema = exports.createMultitenantPrismaMariaDb = exports.createMultitenantPrisma = exports.createAuthMiddleware = exports.createTenantMiddleware = exports.resolveTenantId = void 0;
|
|
3
|
+
exports.EMBED_MODEL = exports.GEMINI_MODEL = exports.EMBED_DIM = exports.GeminiHelp = exports.createGcfHandler = exports.isInternalError = exports.createErrorHandler = exports.getBucket = exports.getDefaultAuth = exports.getTenantAuth = exports.initFirebaseAdmin = exports.getFirebaseStorage = exports.getFirebaseClientApp = exports.createFirebaseClientPlugin = exports.createSwaggerPlugin = exports.DEFAULT_CORS = exports.createCorsPlugin = exports.withTenantSchema = exports.createMultitenantPrismaMariaDb = exports.createMultitenantPrisma = exports.createSharedSecretGuard = exports.safeEqual = exports.isPublicRoute = exports.publicPath = exports.publicRoute = exports.RequestScopeError = exports.runInRequestScope = exports.getRequestScope = exports.requestScoped = exports.createRequestScopePlugin = exports.createTenantMembershipAssert = exports.createAuthMiddleware = exports.InvalidTenantError = exports.createTenantMiddleware = exports.readRawTenantId = exports.resolveTenantId = void 0;
|
|
4
4
|
var resolve_1 = require("./tenant/resolve");
|
|
5
5
|
Object.defineProperty(exports, "resolveTenantId", { enumerable: true, get: function () { return resolve_1.resolveTenantId; } });
|
|
6
|
+
Object.defineProperty(exports, "readRawTenantId", { enumerable: true, get: function () { return resolve_1.readRawTenantId; } });
|
|
6
7
|
var middleware_1 = require("./tenant/middleware");
|
|
7
8
|
Object.defineProperty(exports, "createTenantMiddleware", { enumerable: true, get: function () { return middleware_1.createTenantMiddleware; } });
|
|
9
|
+
Object.defineProperty(exports, "InvalidTenantError", { enumerable: true, get: function () { return middleware_1.InvalidTenantError; } });
|
|
8
10
|
var auth_1 = require("./tenant/auth");
|
|
9
11
|
Object.defineProperty(exports, "createAuthMiddleware", { enumerable: true, get: function () { return auth_1.createAuthMiddleware; } });
|
|
12
|
+
var membership_1 = require("./tenant/membership");
|
|
13
|
+
Object.defineProperty(exports, "createTenantMembershipAssert", { enumerable: true, get: function () { return membership_1.createTenantMembershipAssert; } });
|
|
14
|
+
var request_scope_1 = require("./scope/request-scope");
|
|
15
|
+
Object.defineProperty(exports, "createRequestScopePlugin", { enumerable: true, get: function () { return request_scope_1.createRequestScopePlugin; } });
|
|
16
|
+
Object.defineProperty(exports, "requestScoped", { enumerable: true, get: function () { return request_scope_1.requestScoped; } });
|
|
17
|
+
Object.defineProperty(exports, "getRequestScope", { enumerable: true, get: function () { return request_scope_1.getRequestScope; } });
|
|
18
|
+
Object.defineProperty(exports, "runInRequestScope", { enumerable: true, get: function () { return request_scope_1.runInRequestScope; } });
|
|
19
|
+
Object.defineProperty(exports, "RequestScopeError", { enumerable: true, get: function () { return request_scope_1.RequestScopeError; } });
|
|
20
|
+
var public_route_1 = require("./http/public-route");
|
|
21
|
+
Object.defineProperty(exports, "publicRoute", { enumerable: true, get: function () { return public_route_1.publicRoute; } });
|
|
22
|
+
Object.defineProperty(exports, "publicPath", { enumerable: true, get: function () { return public_route_1.publicPath; } });
|
|
23
|
+
Object.defineProperty(exports, "isPublicRoute", { enumerable: true, get: function () { return public_route_1.isPublicRoute; } });
|
|
24
|
+
Object.defineProperty(exports, "safeEqual", { enumerable: true, get: function () { return public_route_1.safeEqual; } });
|
|
25
|
+
var shared_secret_1 = require("./http/shared-secret");
|
|
26
|
+
Object.defineProperty(exports, "createSharedSecretGuard", { enumerable: true, get: function () { return shared_secret_1.createSharedSecretGuard; } });
|
|
10
27
|
var multitenant_1 = require("./prisma/multitenant");
|
|
11
28
|
Object.defineProperty(exports, "createMultitenantPrisma", { enumerable: true, get: function () { return multitenant_1.createMultitenantPrisma; } });
|
|
12
29
|
var mariadb_factory_1 = require("./prisma/mariadb-factory");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,4CAAkE;AAA1D,0GAAA,eAAe,OAAA;AAAE,0GAAA,eAAe,OAAA;AAGxC,kDAA+E;AAAvE,oHAAA,sBAAsB,OAAA;AAAE,gHAAA,kBAAkB,OAAA;AAGlD,sCAAmD;AAA3C,4GAAA,oBAAoB,OAAA;AAG5B,kDAAiE;AAAzD,0HAAA,4BAA4B,OAAA;AAGpC,uDAAqI;AAA7H,yHAAA,wBAAwB,OAAA;AAAE,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AAAE,kHAAA,iBAAiB,OAAA;AAAE,kHAAA,iBAAiB,OAAA;AAGtG,oDAAsF;AAA9E,2GAAA,WAAW,OAAA;AAAE,0GAAA,UAAU,OAAA;AAAE,6GAAA,aAAa,OAAA;AAAE,yGAAA,SAAS,OAAA;AACzD,sDAA6D;AAArD,wHAAA,uBAAuB,OAAA;AAI/B,oDAA6D;AAArD,sHAAA,uBAAuB,OAAA;AAC/B,4DAAwE;AAAhE,iIAAA,8BAA8B,OAAA;AAGtC,kDAAqD;AAA7C,8GAAA,gBAAgB,OAAA;AAUxB,wCAA6D;AAArD,0GAAA,gBAAgB,OAAA;AAAE,sGAAA,YAAY,OAAA;AAGtC,2CAAqD;AAA7C,6GAAA,mBAAmB,OAAA;AAG3B,0DAA8G;AAAtG,2HAAA,0BAA0B,OAAA;AAAE,qHAAA,oBAAoB,OAAA;AAAE,mHAAA,kBAAkB,OAAA;AAG5E,0CAA6F;AAArF,0GAAA,iBAAiB,OAAA;AAAE,sGAAA,aAAa,OAAA;AAAE,uGAAA,cAAc,OAAA;AAAE,kGAAA,SAAS,OAAA;AAGnE,sDAAyE;AAAjE,mHAAA,kBAAkB,OAAA;AAAE,gHAAA,eAAe,OAAA;AAG3C,kDAAoD;AAA5C,+GAAA,gBAAgB,OAAA;AAGxB,sCAA6E;AAArE,oGAAA,UAAU,OAAA;AAAE,mGAAA,SAAS,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,qGAAA,WAAW,OAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RequestScopeStore {
|
|
2
|
+
/** Identificador sequencial da request (diagnóstico) */
|
|
3
|
+
id: number;
|
|
4
|
+
/** Instâncias criadas nesta request, por proxy */
|
|
5
|
+
instances: Map<symbol, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare class RequestScopeError extends Error {
|
|
8
|
+
constructor(className: string);
|
|
9
|
+
}
|
|
10
|
+
/** Store da request atual (null fora de request). Útil em testes e diagnóstico. */
|
|
11
|
+
export declare function getRequestScope(): RequestScopeStore | null;
|
|
12
|
+
/** Executa `fn` dentro de um escopo novo (uso em jobs/testes fora do Fastify). */
|
|
13
|
+
export declare function runInRequestScope<T>(fn: () => T): T;
|
|
14
|
+
/**
|
|
15
|
+
* Plugin Fastify: abre o escopo no `onRequest` (antes de auth, tenant e
|
|
16
|
+
* preHandler) e descarta as instâncias no `onResponse`. Registrar ANTES dos
|
|
17
|
+
* controllers — chamando como função (`await plugin(fastify)`) ou via
|
|
18
|
+
* `fastify.register`. Registrar duas vezes no mesmo contexto não duplica o hook.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createRequestScopePlugin(): (fastify: any) => Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Devolve um Proxy que se comporta como uma instância de `Ctor`, mas resolve
|
|
23
|
+
* para a instância da REQUEST ATUAL (criada sob demanda com `args`). Cada
|
|
24
|
+
* chamada de `requestScoped` é um "slot" próprio: dois proxies da mesma
|
|
25
|
+
* classe, na mesma request, são instâncias diferentes; o mesmo proxy, na
|
|
26
|
+
* mesma request, é sempre a mesma instância.
|
|
27
|
+
*/
|
|
28
|
+
export declare function requestScoped<T extends object>(Ctor: new (...args: any[]) => T, ...args: any[]): T;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Escopo por request para services das APIs.
|
|
3
|
+
//
|
|
4
|
+
// PROBLEMA QUE RESOLVE: todos os controllers das APIs instanciam o service UMA
|
|
5
|
+
// vez por plugin (`const service = new XService(fastify)`) e, no preHandler de
|
|
6
|
+
// cada rota, fazem `service.setRequest(request)`. O `request` (e com ele o
|
|
7
|
+
// `request.prisma`, que é o client do TENANT) fica numa propriedade de
|
|
8
|
+
// instância compartilhada por todas as requests da instância. Com concurrency
|
|
9
|
+
// > 1 (o default do Cloud Run gen2 é 80), a request B sobrescreve
|
|
10
|
+
// `this.request` enquanto a request A está num `await`, e a query seguinte de
|
|
11
|
+
// A roda no schema da escola de B.
|
|
12
|
+
//
|
|
13
|
+
// SOLUÇÃO: um AsyncLocalStorage aberto no `onRequest` (o contexto sobrevive a
|
|
14
|
+
// await, setTimeout, streams e callbacks) e `requestScoped(Ctor, ...args)`,
|
|
15
|
+
// um Proxy que entrega uma instância PRÓPRIA da request atual. A troca na API
|
|
16
|
+
// é mecânica: `new XService(fastify)` vira `requestScoped(XService, fastify)`.
|
|
17
|
+
// Handlers, services e o `setRequest` não mudam.
|
|
18
|
+
//
|
|
19
|
+
// Uso fora de uma request (timer, escopo de módulo, job) lança
|
|
20
|
+
// RequestScopeError com o nome da classe: é a proteção desejada — antes,
|
|
21
|
+
// misturava dados em silêncio.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.RequestScopeError = void 0;
|
|
24
|
+
exports.getRequestScope = getRequestScope;
|
|
25
|
+
exports.runInRequestScope = runInRequestScope;
|
|
26
|
+
exports.createRequestScopePlugin = createRequestScopePlugin;
|
|
27
|
+
exports.requestScoped = requestScoped;
|
|
28
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
29
|
+
class RequestScopeError extends Error {
|
|
30
|
+
constructor(className) {
|
|
31
|
+
super(`[request-scope] ${className} usado fora do escopo de request. ` +
|
|
32
|
+
`Services com requestScoped() só podem ser acessados durante o processamento de uma request.`);
|
|
33
|
+
this.name = "RequestScopeError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.RequestScopeError = RequestScopeError;
|
|
37
|
+
const storage = new node_async_hooks_1.AsyncLocalStorage();
|
|
38
|
+
const kInstalled = Symbol.for("@mybuildex/pkg/request-scope-installed");
|
|
39
|
+
let sequence = 0;
|
|
40
|
+
/** Store da request atual (null fora de request). Útil em testes e diagnóstico. */
|
|
41
|
+
function getRequestScope() {
|
|
42
|
+
return storage.getStore() ?? null;
|
|
43
|
+
}
|
|
44
|
+
/** Executa `fn` dentro de um escopo novo (uso em jobs/testes fora do Fastify). */
|
|
45
|
+
function runInRequestScope(fn) {
|
|
46
|
+
return storage.run({ id: ++sequence, instances: new Map() }, fn);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Plugin Fastify: abre o escopo no `onRequest` (antes de auth, tenant e
|
|
50
|
+
* preHandler) e descarta as instâncias no `onResponse`. Registrar ANTES dos
|
|
51
|
+
* controllers — chamando como função (`await plugin(fastify)`) ou via
|
|
52
|
+
* `fastify.register`. Registrar duas vezes no mesmo contexto não duplica o hook.
|
|
53
|
+
*/
|
|
54
|
+
function createRequestScopePlugin() {
|
|
55
|
+
return async function requestScopePlugin(fastify) {
|
|
56
|
+
if (fastify[kInstalled])
|
|
57
|
+
return;
|
|
58
|
+
fastify[kInstalled] = true;
|
|
59
|
+
fastify.addHook("onRequest", (_req, _reply, done) => {
|
|
60
|
+
storage.run({ id: ++sequence, instances: new Map() }, done);
|
|
61
|
+
});
|
|
62
|
+
fastify.addHook("onResponse", (_req, _reply, done) => {
|
|
63
|
+
const store = storage.getStore();
|
|
64
|
+
if (store)
|
|
65
|
+
store.instances.clear();
|
|
66
|
+
done();
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Devolve um Proxy que se comporta como uma instância de `Ctor`, mas resolve
|
|
72
|
+
* para a instância da REQUEST ATUAL (criada sob demanda com `args`). Cada
|
|
73
|
+
* chamada de `requestScoped` é um "slot" próprio: dois proxies da mesma
|
|
74
|
+
* classe, na mesma request, são instâncias diferentes; o mesmo proxy, na
|
|
75
|
+
* mesma request, é sempre a mesma instância.
|
|
76
|
+
*/
|
|
77
|
+
function requestScoped(Ctor, ...args) {
|
|
78
|
+
const slot = Symbol(Ctor.name);
|
|
79
|
+
const resolve = () => {
|
|
80
|
+
const store = storage.getStore();
|
|
81
|
+
if (!store)
|
|
82
|
+
throw new RequestScopeError(Ctor.name || "Service");
|
|
83
|
+
let instance = store.instances.get(slot);
|
|
84
|
+
if (!instance) {
|
|
85
|
+
instance = new Ctor(...args);
|
|
86
|
+
store.instances.set(slot, instance);
|
|
87
|
+
}
|
|
88
|
+
return instance;
|
|
89
|
+
};
|
|
90
|
+
// O alvo tem o protótipo da classe para `instanceof` funcionar sem
|
|
91
|
+
// resolver o escopo (checagens estruturais fora de request não lançam).
|
|
92
|
+
const target = Object.create(Ctor.prototype);
|
|
93
|
+
return new Proxy(target, {
|
|
94
|
+
get(_t, prop) {
|
|
95
|
+
const instance = resolve();
|
|
96
|
+
const value = Reflect.get(instance, prop, instance);
|
|
97
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
98
|
+
},
|
|
99
|
+
set(_t, prop, value) {
|
|
100
|
+
const instance = resolve();
|
|
101
|
+
return Reflect.set(instance, prop, value, instance);
|
|
102
|
+
},
|
|
103
|
+
has(_t, prop) {
|
|
104
|
+
return Reflect.has(resolve(), prop);
|
|
105
|
+
},
|
|
106
|
+
deleteProperty(_t, prop) {
|
|
107
|
+
return Reflect.deleteProperty(resolve(), prop);
|
|
108
|
+
},
|
|
109
|
+
ownKeys() {
|
|
110
|
+
return Reflect.ownKeys(resolve());
|
|
111
|
+
},
|
|
112
|
+
getOwnPropertyDescriptor(_t, prop) {
|
|
113
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(resolve(), prop);
|
|
114
|
+
if (descriptor)
|
|
115
|
+
descriptor.configurable = true;
|
|
116
|
+
return descriptor;
|
|
117
|
+
},
|
|
118
|
+
getPrototypeOf() {
|
|
119
|
+
return Ctor.prototype;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=request-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-scope.js","sourceRoot":"","sources":["../../src/scope/request-scope.ts"],"names":[],"mappings":"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"}
|
package/dist/tenant/auth.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { PublicRouteLike } from "../http/public-route";
|
|
1
2
|
export interface TenantAuthLike {
|
|
2
3
|
verifyIdToken(token: string, checkRevoked?: boolean): Promise<any>;
|
|
3
4
|
}
|
|
4
5
|
export interface AuthMiddlewareOptions {
|
|
5
|
-
/** Rotas públicas
|
|
6
|
-
|
|
6
|
+
/** Rotas públicas — dispensam token. Regex sobre o PATHNAME (sem query
|
|
7
|
+
* string) ou regra `{pattern, methods}` gerada por publicRoute()/publicPath(),
|
|
8
|
+
* que também restringe o método HTTP. */
|
|
9
|
+
publicRoutes: PublicRouteLike[];
|
|
7
10
|
/** Resolve o Auth do tenant Firebase (ex: getTenantAuth do pkg) */
|
|
8
11
|
getTenantAuth: (tenantId: string | undefined) => TenantAuthLike;
|
|
9
12
|
/** false = API single-tenant (ex: core): não lê o claim firebase.tenant e
|
package/dist/tenant/auth.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// um token forjado para outro tenant não verifica.
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.createAuthMiddleware = createAuthMiddleware;
|
|
18
|
+
const public_route_1 = require("../http/public-route");
|
|
18
19
|
function createAuthMiddleware(options) {
|
|
19
20
|
const TOKEN_CACHE = new Map();
|
|
20
21
|
const MAX_CACHE_SIZE = options.cacheMaxSize ?? 2000;
|
|
@@ -52,7 +53,7 @@ function createAuthMiddleware(options) {
|
|
|
52
53
|
// (GET protegido → 401, mas `...?cb=/webhook` → 200 sem token).
|
|
53
54
|
const rawUrl = request.raw?.url || "";
|
|
54
55
|
const pathname = rawUrl.split("?")[0];
|
|
55
|
-
if (options.publicRoutes
|
|
56
|
+
if ((0, public_route_1.isPublicRoute)(options.publicRoutes, pathname, request.method) || pathname === "" || pathname === "/") {
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
59
|
let authHeader = request.headers?.authorization;
|
package/dist/tenant/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/tenant/auth.ts"],"names":[],"mappings":";AAAA,8DAA8D;AAC9D,EAAE;AACF,qEAAqE;AACrE,4EAA4E;AAC5E,uEAAuE;AACvE,wEAAwE;AACxE,0EAA0E;AAC1E,6EAA6E;AAC7E,0EAA0E;AAC1E,oEAAoE;AACpE,EAAE;AACF,uEAAuE;AACvE,wEAAwE;AACxE,mDAAmD;;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/tenant/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TenantRequestLike } from "./resolve";
|
|
2
|
+
export interface TenantMembershipOptions {
|
|
3
|
+
/** Client Prisma da escola (ex: mt.getTenantClient do pkg) */
|
|
4
|
+
getTenantClient: (idSchool: string) => any;
|
|
5
|
+
/** TTL do cache positivo, em ms (default 120 s) */
|
|
6
|
+
cacheTtlMs?: number;
|
|
7
|
+
/** Tamanho máximo do cache (default 5000) */
|
|
8
|
+
cacheMaxSize?: number;
|
|
9
|
+
/** Nome do model Prisma com a coluna firebase_id (default "users") */
|
|
10
|
+
model?: string;
|
|
11
|
+
/** Log de falha de consulta (default console.error) */
|
|
12
|
+
log?: (message: string, error?: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface TenantMembershipAssert {
|
|
15
|
+
(decodedToken: any, request: TenantRequestLike): Promise<void>;
|
|
16
|
+
/** Limpa o cache (testes / rotação manual) */
|
|
17
|
+
clearCache(): void;
|
|
18
|
+
/** Quantidade de entradas em cache (diagnóstico) */
|
|
19
|
+
cacheSize(): number;
|
|
20
|
+
}
|
|
21
|
+
export declare function createTenantMembershipAssert(options: TenantMembershipOptions): TenantMembershipAssert;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Vínculo do token ao id_school da URL (o gancho `assertTenant` pronto).
|
|
3
|
+
//
|
|
4
|
+
// O auth middleware valida a ASSINATURA do token no tenant Firebase do
|
|
5
|
+
// próprio token, mas nada amarra esse usuário ao `:id_school` da URL: um
|
|
6
|
+
// token válido da escola A servia em `/v1/B/...`. Este assert consulta a
|
|
7
|
+
// tabela `users` do schema da escola da URL por `firebase_id = uid` e
|
|
8
|
+
// `deleted_at is null`; sem vínculo, lança (o middleware responde 403).
|
|
9
|
+
//
|
|
10
|
+
// Regras do domínio MyBuilder: cada escola tem o próprio tenant Firebase e
|
|
11
|
+
// nunca compartilha usuário — por isso não há caso "usuário em duas escolas".
|
|
12
|
+
//
|
|
13
|
+
// Fail-closed: erro de banco na consulta NEGA e loga. Cache em memória por
|
|
14
|
+
// `idSchool:uid` só para resultados positivos, com TTL igual ao do cache de
|
|
15
|
+
// tokens (120 s), para não repetir a consulta a cada request.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.createTenantMembershipAssert = createTenantMembershipAssert;
|
|
18
|
+
const resolve_1 = require("./resolve");
|
|
19
|
+
function createTenantMembershipAssert(options) {
|
|
20
|
+
const TTL_MS = options.cacheTtlMs ?? 120 * 1000;
|
|
21
|
+
const MAX_SIZE = options.cacheMaxSize ?? 5000;
|
|
22
|
+
const MODEL = options.model ?? "users";
|
|
23
|
+
const log = options.log ?? ((message, error) => console.error(message, error ?? ""));
|
|
24
|
+
const cache = new Map();
|
|
25
|
+
function remember(key) {
|
|
26
|
+
cache.set(key, Date.now() + TTL_MS);
|
|
27
|
+
if (cache.size > MAX_SIZE) {
|
|
28
|
+
const it = cache.keys();
|
|
29
|
+
for (let i = 0; i < 100 && cache.size > MAX_SIZE; i++) {
|
|
30
|
+
const k = it.next().value;
|
|
31
|
+
if (k === undefined)
|
|
32
|
+
break;
|
|
33
|
+
cache.delete(k);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isRemembered(key) {
|
|
38
|
+
const exp = cache.get(key);
|
|
39
|
+
if (exp === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
if (exp <= Date.now()) {
|
|
42
|
+
cache.delete(key);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const assert = (async (decodedToken, request) => {
|
|
48
|
+
const idSchool = (0, resolve_1.resolveTenantId)(request);
|
|
49
|
+
// Sem tenant na URL (raiz, documentação): nada a amarrar.
|
|
50
|
+
if (!idSchool)
|
|
51
|
+
return;
|
|
52
|
+
const uid = decodedToken?.uid;
|
|
53
|
+
if (!uid)
|
|
54
|
+
throw new Error("Token sem uid");
|
|
55
|
+
const key = `${idSchool}:${uid}`;
|
|
56
|
+
if (isRemembered(key))
|
|
57
|
+
return;
|
|
58
|
+
let found;
|
|
59
|
+
try {
|
|
60
|
+
const client = options.getTenantClient(idSchool);
|
|
61
|
+
found = await client[MODEL].findFirst({
|
|
62
|
+
where: { firebase_id: uid, deleted_at: null },
|
|
63
|
+
select: { id: true },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
log(`[tenant-membership] falha ao consultar ${MODEL} da escola ${idSchool}; negando acesso`, error);
|
|
68
|
+
throw new Error("Falha ao verificar vínculo com a escola");
|
|
69
|
+
}
|
|
70
|
+
if (!found)
|
|
71
|
+
throw new Error(`Usuário ${uid} sem vínculo com a escola ${idSchool}`);
|
|
72
|
+
remember(key);
|
|
73
|
+
});
|
|
74
|
+
assert.clearCache = () => cache.clear();
|
|
75
|
+
assert.cacheSize = () => cache.size;
|
|
76
|
+
return assert;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=membership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"membership.js","sourceRoot":"","sources":["../../src/tenant/membership.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,5 +5,12 @@ export interface TenantMiddlewareOptions<TClient> {
|
|
|
5
5
|
* quando o request caiu no default. Erros aqui NÃO derrubam o request
|
|
6
6
|
* (log + segue) — enriquecimento é best-effort. */
|
|
7
7
|
onTenantResolved?: (req: any, idSchool: string | null) => void | Promise<void>;
|
|
8
|
+
/** id_school presente mas inválido => 400 (default: false, mantém o
|
|
9
|
+
* comportamento histórico de cair no default). */
|
|
10
|
+
strictTenant?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class InvalidTenantError extends Error {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
constructor(raw: unknown);
|
|
8
15
|
}
|
|
9
16
|
export declare function createTenantMiddleware<TClient>(options: TenantMiddlewareOptions<TClient>): (fastify: any) => Promise<void>;
|
|
@@ -8,9 +8,32 @@
|
|
|
8
8
|
// Também seta req.id_school (número; undefined quando sem id válido) e
|
|
9
9
|
// oferece onTenantResolved para enriquecer o request (ex: carregar
|
|
10
10
|
// permissões do usuário) sem cada API duplicar lógica no middleware.
|
|
11
|
+
//
|
|
12
|
+
// strictTenant (opt-in): quando o request TRAZ um id_school (params, query ou
|
|
13
|
+
// header) mas ele é inválido ("abc", "0", "-3", "12abc"), responde 400 em vez
|
|
14
|
+
// de cair no default. O default é o database do DATABASE_URL — uma escola
|
|
15
|
+
// real — e as rotas sem schema de params não validam o :id_school, então
|
|
16
|
+
// `PUT /v1/abc/.../restore/5` executava na escola 1 sem erro. Sem id_school
|
|
17
|
+
// nenhum (raiz, documentação) o default continua valendo.
|
|
11
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.InvalidTenantError = void 0;
|
|
12
20
|
exports.createTenantMiddleware = createTenantMiddleware;
|
|
13
21
|
const resolve_1 = require("./resolve");
|
|
22
|
+
class InvalidTenantError extends Error {
|
|
23
|
+
statusCode = 400;
|
|
24
|
+
constructor(raw) {
|
|
25
|
+
super(`id_school inválido: ${String(raw)}`);
|
|
26
|
+
this.name = "InvalidTenantError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.InvalidTenantError = InvalidTenantError;
|
|
30
|
+
function hasRawTenant(raw) {
|
|
31
|
+
if (raw === undefined || raw === null)
|
|
32
|
+
return false;
|
|
33
|
+
if (typeof raw === "string" && raw.trim() === "")
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
14
37
|
function createTenantMiddleware(options) {
|
|
15
38
|
return async function tenantMiddleware(fastify) {
|
|
16
39
|
fastify.addHook("preValidation", async (req) => {
|
|
@@ -20,6 +43,12 @@ function createTenantMiddleware(options) {
|
|
|
20
43
|
req.id_school = Number(idSchool);
|
|
21
44
|
}
|
|
22
45
|
else {
|
|
46
|
+
const raw = (0, resolve_1.readRawTenantId)(req);
|
|
47
|
+
if (options.strictTenant && hasRawTenant(raw)) {
|
|
48
|
+
// Sem client atribuído: o error handler responde 400 e
|
|
49
|
+
// nenhum handler roda contra a escola default.
|
|
50
|
+
throw new InvalidTenantError(raw);
|
|
51
|
+
}
|
|
23
52
|
req.prisma = options.defaultClient;
|
|
24
53
|
}
|
|
25
54
|
if (options.onTenantResolved) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/tenant/middleware.ts"],"names":[],"mappings":";AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,2BAA2B;AAC3B,EAAE;AACF,uEAAuE;AACvE,mEAAmE;AACnE,qEAAqE
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/tenant/middleware.ts"],"names":[],"mappings":";AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,2BAA2B;AAC3B,EAAE;AACF,uEAAuE;AACvE,mEAAmE;AACnE,qEAAqE;AACrE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,0EAA0E;AAC1E,yEAAyE;AACzE,4EAA4E;AAC5E,0DAA0D;;;AA+B1D,wDA4BC;AAzDD,uCAA8E;AAc9E,MAAa,kBAAmB,SAAQ,KAAK;IACzC,UAAU,GAAG,GAAG,CAAC;IAEjB,YAAY,GAAY;QACpB,KAAK,CAAC,uBAAuB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACrC,CAAC;CACJ;AAPD,gDAOC;AAED,SAAS,YAAY,CAAC,GAAY;IAC9B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACpD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC/D,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,SAAgB,sBAAsB,CAAU,OAAyC;IACrF,OAAO,KAAK,UAAU,gBAAgB,CAAC,OAAY;QAC/C,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,EAAE,GAGvC,EAAE,EAAE;YACD,MAAM,QAAQ,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAC;YACtC,IAAI,QAAQ,EAAE,CAAC;gBACX,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;gBAC/C,GAAG,CAAC,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;YACrC,CAAC;iBAAM,CAAC;gBACJ,MAAM,GAAG,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAC;gBACjC,IAAI,OAAO,CAAC,YAAY,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;oBAC5C,uDAAuD;oBACvD,+CAA+C;oBAC/C,MAAM,IAAI,kBAAkB,CAAC,GAAG,CAAC,CAAC;gBACtC,CAAC;gBACD,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;YACvC,CAAC;YACD,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBACD,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;gBAClD,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACT,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,CAAC,CAAC,CAAC;gBAChD,CAAC;YACL,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;AACN,CAAC"}
|
package/dist/tenant/resolve.d.ts
CHANGED
|
@@ -3,4 +3,7 @@ export interface TenantRequestLike {
|
|
|
3
3
|
query?: Record<string, unknown> | null;
|
|
4
4
|
headers?: Record<string, unknown> | null;
|
|
5
5
|
}
|
|
6
|
+
/** Valor bruto de id_school como veio no request (undefined quando ausente em
|
|
7
|
+
* params, query e header). Serve para distinguir "não informado" de "inválido". */
|
|
8
|
+
export declare function readRawTenantId(req: TenantRequestLike): unknown;
|
|
6
9
|
export declare function resolveTenantId(req: TenantRequestLike): string | null;
|
package/dist/tenant/resolve.js
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
// Só aceita inteiro positivo em string de dígitos — qualquer outra coisa é
|
|
5
5
|
// tratada como "sem tenant" (o chamador decide o fallback), nunca interpolada em SQL.
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.readRawTenantId = readRawTenantId;
|
|
7
8
|
exports.resolveTenantId = resolveTenantId;
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/** Valor bruto de id_school como veio no request (undefined quando ausente em
|
|
10
|
+
* params, query e header). Serve para distinguir "não informado" de "inválido". */
|
|
11
|
+
function readRawTenantId(req) {
|
|
12
|
+
return req.params?.id_school ??
|
|
10
13
|
req.query?.id_school ??
|
|
11
14
|
req.headers?.["x-id-school"];
|
|
15
|
+
}
|
|
16
|
+
function resolveTenantId(req) {
|
|
17
|
+
const raw = readRawTenantId(req);
|
|
12
18
|
const idStr = typeof raw === "string" ? raw : raw != null ? String(raw) : "";
|
|
13
19
|
if (idStr && /^\d+$/.test(idStr) && Number(idStr) > 0)
|
|
14
20
|
return idStr;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/tenant/resolve.ts"],"names":[],"mappings":";AAAA,2CAA2C;AAC3C,4GAA4G;AAC5G,2EAA2E;AAC3E,sFAAsF;;
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/tenant/resolve.ts"],"names":[],"mappings":";AAAA,2CAA2C;AAC3C,4GAA4G;AAC5G,2EAA2E;AAC3E,sFAAsF;;AAUtF,0CAIC;AAED,0CAMC;AAdD;oFACoF;AACpF,SAAgB,eAAe,CAAC,GAAsB;IAClD,OAAQ,GAAG,CAAC,MAAc,EAAE,SAAS;QAChC,GAAG,CAAC,KAAa,EAAE,SAAS;QAC5B,GAAG,CAAC,OAAe,EAAE,CAAC,aAAa,CAAC,CAAC;AAC9C,CAAC;AAED,SAAgB,eAAe,CAAC,GAAsB;IAClD,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAEjC,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,IAAI,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACpE,OAAO,IAAI,CAAC;AAChB,CAAC"}
|