@mybuildex/pkg 0.2.1
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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/ai/gemini.d.ts +24 -0
- package/dist/ai/gemini.js +160 -0
- package/dist/ai/gemini.js.map +1 -0
- package/dist/cors/plugin.d.ts +6 -0
- package/dist/cors/plugin.js +51 -0
- package/dist/cors/plugin.js.map +1 -0
- package/dist/firebase/admin.d.ts +7 -0
- package/dist/firebase/admin.js +50 -0
- package/dist/firebase/admin.js.map +1 -0
- package/dist/firebase/client-plugin.d.ts +5 -0
- package/dist/firebase/client-plugin.js +27 -0
- package/dist/firebase/client-plugin.js.map +1 -0
- package/dist/http/error-handler.d.ts +8 -0
- package/dist/http/error-handler.js +33 -0
- package/dist/http/error-handler.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/prisma/mariadb-factory.d.ts +4 -0
- package/dist/prisma/mariadb-factory.js +20 -0
- package/dist/prisma/mariadb-factory.js.map +1 -0
- package/dist/prisma/multitenant.d.ts +55 -0
- package/dist/prisma/multitenant.js +76 -0
- package/dist/prisma/multitenant.js.map +1 -0
- package/dist/swagger/plugin.d.ts +7 -0
- package/dist/swagger/plugin.js +56 -0
- package/dist/swagger/plugin.js.map +1 -0
- package/dist/tenant/auth.d.ts +21 -0
- package/dist/tenant/auth.js +107 -0
- package/dist/tenant/auth.js.map +1 -0
- package/dist/tenant/middleware.d.ts +5 -0
- package/dist/tenant/middleware.js +20 -0
- package/dist/tenant/middleware.js.map +1 -0
- package/dist/tenant/resolve.d.ts +6 -0
- package/dist/tenant/resolve.js +17 -0
- package/dist/tenant/resolve.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabriel Alencar / MyBuilder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @mybuildex/pkg
|
|
2
|
+
|
|
3
|
+
Infra comum das APIs MyBuilder (fonte única): multitenancy Prisma com **pool único** compartilhado entre escolas, middleware de tenant (`:id_school` da URL decide o schema) e CORS padrão.
|
|
4
|
+
|
|
5
|
+
## Módulos
|
|
6
|
+
|
|
7
|
+
- `createMultitenantPrisma` — fábrica: 1 pool mariadb para a instância inteira; client por escola é leve e só qualifica o database no SQL gerado. Pool **sem database default** (raw sem qualificar falha alto — Error 1046 — em vez de vazar para o tenant errado). Suporta `?socket=/cloudsql/...` (prod) e `host:porta` (dev/túnel).
|
|
8
|
+
- `createTenantMiddleware` — hook `preValidation` que injeta `req.prisma` resolvendo `params.id_school → query.id_school → header x-id-school` (só inteiro positivo; inválido cai no default).
|
|
9
|
+
- `resolveTenantId` — a regra de resolução, exportada para testes/usos avulsos.
|
|
10
|
+
- `createCorsPlugin` — CORS padrão das APIs.
|
|
11
|
+
|
|
12
|
+
## Uso (numa API)
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import mariadb from "mariadb";
|
|
16
|
+
import {PrismaMariaDb} from "@prisma/adapter-mariadb";
|
|
17
|
+
import {PrismaClient} from "./generated/client";
|
|
18
|
+
import {createMultitenantPrisma, createTenantMiddleware} from "@mybuildex/pkg";
|
|
19
|
+
|
|
20
|
+
const mt = createMultitenantPrisma({
|
|
21
|
+
databaseUrl: process.env.DATABASE_URL!,
|
|
22
|
+
PrismaClient, PrismaMariaDb, createPool: mariadb.createPool,
|
|
23
|
+
});
|
|
24
|
+
// fastify.register(mt.plugin) + fastify.register(createTenantMiddleware(mt))
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A lib **não** depende de `@prisma/client`/`mariadb`: cada API injeta o client gerado e o driver nas versões dela.
|
|
28
|
+
|
|
29
|
+
## Consumo pelas APIs (npmjs público)
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm i @mybuildex/pkg
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Release
|
|
38
|
+
|
|
39
|
+
1. Bump da versão no `package.json` (`npm version patch|minor` local, sem push automático).
|
|
40
|
+
2. Commit + tag `vX.Y.Z` + push da tag → o workflow [publish.yml](.github/workflows/publish.yml) roda testes e publica (secret `NPM_TOKEN`).
|
|
41
|
+
3. Nas APIs: `npm update @mybuildex/pkg` (ou bump da range no package.json).
|
|
42
|
+
|
|
43
|
+
## Testes
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm test
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Inclui o teste de isolamento ponta a ponta (`test/isolation.spec.ts`): URL → middleware → prisma, com 50 requests concorrentes intercalando escolas — dados de uma escola **nunca** aparecem na resposta de outra.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const EMBED_DIM: number;
|
|
2
|
+
export declare const GEMINI_MODEL: string;
|
|
3
|
+
export declare const EMBED_MODEL: string;
|
|
4
|
+
export declare class GeminiHelp {
|
|
5
|
+
static isConfigured(): boolean;
|
|
6
|
+
static getClient(): any;
|
|
7
|
+
static withRetry<T>(fn: () => Promise<T>, tries?: number, baseDelayMs?: number): Promise<T>;
|
|
8
|
+
static uploadAudio(path: string, mimeType?: string): Promise<any>;
|
|
9
|
+
static deleteFile(name: string): Promise<void>;
|
|
10
|
+
static generateJson(parts: any[], config?: any): Promise<any>;
|
|
11
|
+
/**
|
|
12
|
+
* Geracao em STREAMING (texto puro, sem responseMimeType JSON — JSON nao
|
|
13
|
+
* streama bem). Chama onChunk a cada pedaco; para cedo se shouldStop()
|
|
14
|
+
* (cliente desconectou). Retorna o texto completo + usage do final.
|
|
15
|
+
*/
|
|
16
|
+
static generateTextStream(parts: any[], config: any, onChunk: (text: string) => void, shouldStop?: () => boolean): Promise<{
|
|
17
|
+
text: string;
|
|
18
|
+
usage: any;
|
|
19
|
+
}>;
|
|
20
|
+
static filePart(file: any): any;
|
|
21
|
+
static normalize(vec: number[]): number[];
|
|
22
|
+
static embed(texts: string[], taskType: string): Promise<number[][]>;
|
|
23
|
+
static cosine(a: number[], b: number[]): number;
|
|
24
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Helper Gemini compartilhado (transcrição, embeddings, chat RAG).
|
|
3
|
+
// `@google/genai` é peer dep, carregado sob demanda — APIs que não usam IA
|
|
4
|
+
// não pagam o import. Chave SEMPRE via env GEMINI_API_KEY.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GeminiHelp = exports.EMBED_MODEL = exports.GEMINI_MODEL = exports.EMBED_DIM = void 0;
|
|
7
|
+
// Dimensão fixa do índice: mudar exige reprocessar todos os cursos.
|
|
8
|
+
exports.EMBED_DIM = parseInt(process.env.AI_EMBED_DIM || "768");
|
|
9
|
+
exports.GEMINI_MODEL = process.env.AI_GEMINI_MODEL || "gemini-2.5-flash";
|
|
10
|
+
exports.EMBED_MODEL = process.env.AI_EMBED_MODEL || "gemini-embedding-001";
|
|
11
|
+
const MAX_EMBED_BATCH = 100;
|
|
12
|
+
let client = null;
|
|
13
|
+
let genaiModule = null;
|
|
14
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
15
|
+
function genai() {
|
|
16
|
+
if (!genaiModule)
|
|
17
|
+
genaiModule = require("@google/genai");
|
|
18
|
+
return genaiModule;
|
|
19
|
+
}
|
|
20
|
+
class GeminiHelp {
|
|
21
|
+
// Ambientes sem a chave (ex: so dev configurado) degradam com resposta
|
|
22
|
+
// amigavel em vez de 500 — checar antes de qualquer chamada ao Gemini.
|
|
23
|
+
static isConfigured() {
|
|
24
|
+
return !!process.env.GEMINI_API_KEY;
|
|
25
|
+
}
|
|
26
|
+
static getClient() {
|
|
27
|
+
if (!process.env.GEMINI_API_KEY)
|
|
28
|
+
throw new Error("GEMINI_API_KEY nao configurada");
|
|
29
|
+
if (!client)
|
|
30
|
+
client = new (genai().GoogleGenAI)({ apiKey: process.env.GEMINI_API_KEY });
|
|
31
|
+
return client;
|
|
32
|
+
}
|
|
33
|
+
static async withRetry(fn, tries = 3, baseDelayMs = 1500) {
|
|
34
|
+
let lastErr;
|
|
35
|
+
for (let i = 0; i < tries; i++) {
|
|
36
|
+
try {
|
|
37
|
+
return await fn();
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
lastErr = e;
|
|
41
|
+
const status = e?.status || e?.code;
|
|
42
|
+
// 4xx (exceto 429) nao adianta repetir
|
|
43
|
+
if (status && status !== 429 && status < 500)
|
|
44
|
+
throw e;
|
|
45
|
+
await new Promise(r => setTimeout(r, baseDelayMs * (i + 1)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw lastErr;
|
|
49
|
+
}
|
|
50
|
+
static async uploadAudio(path, mimeType = "audio/mp3") {
|
|
51
|
+
const ai = GeminiHelp.getClient();
|
|
52
|
+
let file = await ai.files.upload({ file: path, config: { mimeType } });
|
|
53
|
+
const started = Date.now();
|
|
54
|
+
while (file.state === "PROCESSING" && (Date.now() - started) < 120000) {
|
|
55
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
56
|
+
file = await ai.files.get({ name: file.name });
|
|
57
|
+
}
|
|
58
|
+
if (file.state !== "ACTIVE")
|
|
59
|
+
throw new Error(`Gemini file nao ficou ACTIVE (state=${file.state})`);
|
|
60
|
+
return file;
|
|
61
|
+
}
|
|
62
|
+
static async deleteFile(name) {
|
|
63
|
+
try {
|
|
64
|
+
await GeminiHelp.getClient().files.delete({ name });
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
console.log("gemini deleteFile", e);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
static async generateJson(parts, config = {}) {
|
|
71
|
+
const ai = GeminiHelp.getClient();
|
|
72
|
+
const call = async () => {
|
|
73
|
+
const resp = await ai.models.generateContent({
|
|
74
|
+
model: exports.GEMINI_MODEL,
|
|
75
|
+
contents: genai().createUserContent(parts),
|
|
76
|
+
config: { responseMimeType: "application/json", temperature: 0.2, ...config },
|
|
77
|
+
});
|
|
78
|
+
const text = (resp.text || "").replace(/^```(json)?/m, "").replace(/```$/m, "").trim();
|
|
79
|
+
return { json: JSON.parse(text), usage: resp.usageMetadata };
|
|
80
|
+
};
|
|
81
|
+
// parse defensivo: 1 retry extra alem dos retries de rede
|
|
82
|
+
return await GeminiHelp.withRetry(async () => {
|
|
83
|
+
try {
|
|
84
|
+
return await call();
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (e instanceof SyntaxError)
|
|
88
|
+
return await call();
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Geracao em STREAMING (texto puro, sem responseMimeType JSON — JSON nao
|
|
95
|
+
* streama bem). Chama onChunk a cada pedaco; para cedo se shouldStop()
|
|
96
|
+
* (cliente desconectou). Retorna o texto completo + usage do final.
|
|
97
|
+
*/
|
|
98
|
+
static async generateTextStream(parts, config, onChunk, shouldStop) {
|
|
99
|
+
const ai = GeminiHelp.getClient();
|
|
100
|
+
const stream = await GeminiHelp.withRetry(() => ai.models.generateContentStream({
|
|
101
|
+
model: exports.GEMINI_MODEL,
|
|
102
|
+
contents: genai().createUserContent(parts),
|
|
103
|
+
config: { temperature: 0.2, ...config },
|
|
104
|
+
}));
|
|
105
|
+
let full = "";
|
|
106
|
+
let usage = null;
|
|
107
|
+
for await (const chunk of stream) {
|
|
108
|
+
if (shouldStop?.())
|
|
109
|
+
break;
|
|
110
|
+
const text = chunk.text || "";
|
|
111
|
+
if (text) {
|
|
112
|
+
full += text;
|
|
113
|
+
onChunk(text);
|
|
114
|
+
}
|
|
115
|
+
if (chunk.usageMetadata)
|
|
116
|
+
usage = chunk.usageMetadata;
|
|
117
|
+
}
|
|
118
|
+
return { text: full, usage };
|
|
119
|
+
}
|
|
120
|
+
static filePart(file) {
|
|
121
|
+
return genai().createPartFromUri(file.uri, file.mimeType);
|
|
122
|
+
}
|
|
123
|
+
static normalize(vec) {
|
|
124
|
+
let norm = 0;
|
|
125
|
+
for (const v of vec)
|
|
126
|
+
norm += v * v;
|
|
127
|
+
norm = Math.sqrt(norm) || 1;
|
|
128
|
+
return vec.map(v => v / norm);
|
|
129
|
+
}
|
|
130
|
+
// Embeddings em lote. taskType: RETRIEVAL_DOCUMENT (indexacao) | RETRIEVAL_QUERY (pergunta).
|
|
131
|
+
static async embed(texts, taskType) {
|
|
132
|
+
const ai = GeminiHelp.getClient();
|
|
133
|
+
const out = [];
|
|
134
|
+
for (let i = 0; i < texts.length; i += MAX_EMBED_BATCH) {
|
|
135
|
+
const batch = texts.slice(i, i + MAX_EMBED_BATCH);
|
|
136
|
+
const resp = await GeminiHelp.withRetry(() => ai.models.embedContent({
|
|
137
|
+
model: exports.EMBED_MODEL,
|
|
138
|
+
contents: batch,
|
|
139
|
+
config: { taskType, outputDimensionality: exports.EMBED_DIM },
|
|
140
|
+
}));
|
|
141
|
+
for (const emb of (resp.embeddings || [])) {
|
|
142
|
+
// com outputDimensionality < 3072 o vetor nao vem normalizado
|
|
143
|
+
out.push(GeminiHelp.normalize(emb.values));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (out.length !== texts.length)
|
|
147
|
+
throw new Error("embeddings incompletos");
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
static cosine(a, b) {
|
|
151
|
+
// vetores ja normalizados -> produto escalar
|
|
152
|
+
let dot = 0;
|
|
153
|
+
const n = Math.min(a.length, b.length);
|
|
154
|
+
for (let i = 0; i < n; i++)
|
|
155
|
+
dot += a[i] * b[i];
|
|
156
|
+
return dot;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.GeminiHelp = GeminiHelp;
|
|
160
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/ai/gemini.ts"],"names":[],"mappings":";AAAA,mEAAmE;AACnE,2EAA2E;AAC3E,2DAA2D;;;AAE3D,oEAAoE;AACvD,QAAA,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,KAAK,CAAC,CAAC;AACxD,QAAA,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,kBAAkB,CAAC;AACjE,QAAA,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,sBAAsB,CAAC;AAEhF,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B,IAAI,MAAM,GAAQ,IAAI,CAAC;AACvB,IAAI,WAAW,GAAQ,IAAI,CAAC;AAE5B,uDAAuD;AACvD,SAAS,KAAK;IACV,IAAI,CAAC,WAAW;QAAE,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACzD,OAAO,WAAW,CAAC;AACvB,CAAC;AAED,MAAa,UAAU;IAEnB,uEAAuE;IACvE,uEAAuE;IACvE,MAAM,CAAC,YAAY;QACf,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACxC,CAAC;IAED,MAAM,CAAC,SAAS;QACZ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc;YAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACnF,IAAI,CAAC,MAAM;YAAE,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,WAAW,CAAC,CAAC,EAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,EAAC,CAAC,CAAC;QACtF,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,SAAS,CAAI,EAAoB,EAAE,KAAK,GAAG,CAAC,EAAE,WAAW,GAAG,IAAI;QACzE,IAAI,OAAY,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACD,OAAO,MAAM,EAAE,EAAE,CAAC;YACtB,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBACd,OAAO,GAAG,CAAC,CAAC;gBACZ,MAAM,MAAM,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC,EAAE,IAAI,CAAC;gBACpC,uCAAuC;gBACvC,IAAI,MAAM,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,GAAG,GAAG;oBAAE,MAAM,CAAC,CAAC;gBACtD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,CAAC;QACL,CAAC;QACD,MAAM,OAAO,CAAC;IAClB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,IAAY,EAAE,QAAQ,GAAG,WAAW;QACzD,MAAM,EAAE,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;QAClC,IAAI,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,EAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAC,QAAQ,EAAC,EAAC,CAAC,CAAC;QACnE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,KAAK,KAAK,YAAY,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;YACpE,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;YAC5C,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAC,IAAI,EAAE,IAAI,CAAC,IAAc,EAAC,CAAC,CAAC;QAC3D,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;QACnG,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,IAAY;QAChC,IAAI,CAAC;YACD,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,EAAC,IAAI,EAAC,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC;QACxC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,KAAY,EAAE,SAAc,EAAE;QACpD,MAAM,EAAE,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;YACpB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC;gBACzC,KAAK,EAAE,oBAAY;gBACnB,QAAQ,EAAE,KAAK,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC;gBAC1C,MAAM,EAAE,EAAC,gBAAgB,EAAE,kBAAkB,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,MAAM,EAAC;aAC9E,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACvF,OAAO,EAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,aAAa,EAAC,CAAC;QAC/D,CAAC,CAAC;QACF,0DAA0D;QAC1D,OAAO,MAAM,UAAU,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE;YACzC,IAAI,CAAC;gBACD,OAAO,MAAM,IAAI,EAAE,CAAC;YACxB,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBACd,IAAI,CAAC,YAAY,WAAW;oBAAE,OAAO,MAAM,IAAI,EAAE,CAAC;gBAClD,MAAM,CAAC,CAAC;YACZ,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAC3B,KAAY,EACZ,MAAW,EACX,OAA+B,EAC/B,UAA0B;QAE1B,MAAM,EAAE,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;QAClC,MAAM,MAAM,GAAQ,MAAM,UAAU,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,qBAAqB,CAAC;YACjF,KAAK,EAAE,oBAAY;YACnB,QAAQ,EAAE,KAAK,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC;YAC1C,MAAM,EAAE,EAAC,WAAW,EAAE,GAAG,EAAE,GAAG,MAAM,EAAC;SACxC,CAAC,CAAC,CAAC;QACJ,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,KAAK,GAAQ,IAAI,CAAC;QACtB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC/B,IAAI,UAAU,EAAE,EAAE;gBAAE,MAAM;YAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;YAC9B,IAAI,IAAI,EAAE,CAAC;gBACP,IAAI,IAAI,IAAI,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;YACD,IAAI,KAAK,CAAC,aAAa;gBAAE,KAAK,GAAG,KAAK,CAAC,aAAa,CAAC;QACzD,CAAC;QACD,OAAO,EAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAC,CAAC;IAC/B,CAAC;IAED,MAAM,CAAC,QAAQ,CAAC,IAAS;QACrB,OAAO,KAAK,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAa,EAAE,IAAI,CAAC,QAAkB,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,CAAC,SAAS,CAAC,GAAa;QAC1B,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,KAAK,MAAM,CAAC,IAAI,GAAG;YAAE,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5B,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,6FAA6F;IAC7F,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAe,EAAE,QAAgB;QAChD,MAAM,EAAE,GAAG,UAAU,CAAC,SAAS,EAAE,CAAC;QAClC,MAAM,GAAG,GAAe,EAAE,CAAC;QAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,eAAe,EAAE,CAAC;YACrD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC;YAClD,MAAM,IAAI,GAAQ,MAAM,UAAU,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC;gBACtE,KAAK,EAAE,mBAAW;gBAClB,QAAQ,EAAE,KAAK;gBACf,MAAM,EAAE,EAAC,QAAQ,EAAE,oBAAoB,EAAE,iBAAS,EAAC;aACtD,CAAC,CAAC,CAAC;YACJ,KAAK,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;gBACxC,8DAA8D;gBAC9D,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,MAAkB,CAAC,CAAC,CAAC;YAC3D,CAAC;QACL,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC3E,OAAO,GAAG,CAAC;IACf,CAAC;IAED,MAAM,CAAC,MAAM,CAAC,CAAW,EAAE,CAAW;QAClC,6CAA6C;QAC7C,IAAI,GAAG,GAAG,CAAC,CAAC;QACZ,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,OAAO,GAAG,CAAC;IACf,CAAC;CACJ;AA7ID,gCA6IC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// CORS padrão das APIs MyBuilder. Centralizado para que um ajuste de política
|
|
3
|
+
// (ex: restringir origin por ambiente) seja feito UMA vez.
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.DEFAULT_CORS = void 0;
|
|
39
|
+
exports.createCorsPlugin = createCorsPlugin;
|
|
40
|
+
exports.DEFAULT_CORS = {
|
|
41
|
+
origin: "*",
|
|
42
|
+
methods: ["GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"],
|
|
43
|
+
};
|
|
44
|
+
function createCorsPlugin(options) {
|
|
45
|
+
return async function pluginCors(fastify) {
|
|
46
|
+
// import dinâmico: @fastify/cors é peer dependency opcional
|
|
47
|
+
const cors = (await Promise.resolve().then(() => __importStar(require("@fastify/cors")))).default;
|
|
48
|
+
await fastify.register(cors, { ...exports.DEFAULT_CORS, ...options });
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/cors/plugin.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,2DAA2D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAY3D,4CAMC;AAXY,QAAA,YAAY,GAA0B;IAC/C,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC;CAChE,CAAC;AAEF,SAAgB,gBAAgB,CAAC,OAAqB;IAClD,OAAO,KAAK,UAAU,UAAU,CAAC,OAAY;QACzC,4DAA4D;QAC5D,MAAM,IAAI,GAAG,CAAC,wDAAa,eAAe,GAAC,CAAC,CAAC,OAAO,CAAC;QACrD,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAC,GAAG,oBAAY,EAAE,GAAG,OAAO,EAAC,CAAC,CAAC;IAChE,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface FirebaseAdminOptions {
|
|
2
|
+
/** Caminho do arquivo de service account para dev local (fallback:
|
|
3
|
+
* env FIREBASE_CREDENTIALS_FILE). Ignorado quando ADC está disponível. */
|
|
4
|
+
credentialsFile?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function initFirebaseAdmin(options?: FirebaseAdminOptions): any;
|
|
7
|
+
export declare function getTenantAuth(tenantId: string | undefined): any;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Firebase ADMIN SDK, ADC-first (padrão em produção no GCP — nenhum arquivo
|
|
3
|
+
// de credencial no bundle). Para dev local SEM ADC, o caminho do arquivo de
|
|
4
|
+
// service account vem de env (FIREBASE_CREDENTIALS_FILE) ou de opção — a lib
|
|
5
|
+
// NUNCA embute credencial. `firebase-admin` é peer dep.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.initFirebaseAdmin = initFirebaseAdmin;
|
|
8
|
+
exports.getTenantAuth = getTenantAuth;
|
|
9
|
+
let adminInstance = null;
|
|
10
|
+
function initFirebaseAdmin(options = {}) {
|
|
11
|
+
if (adminInstance)
|
|
12
|
+
return adminInstance;
|
|
13
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
14
|
+
const admin = require("firebase-admin");
|
|
15
|
+
if (!admin.apps.length) {
|
|
16
|
+
const useADC = process.env.USE_ADC === "true" ||
|
|
17
|
+
!!process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
18
|
+
if (useADC) {
|
|
19
|
+
admin.initializeApp(); // ADC: credencial do ambiente
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const file = options.credentialsFile || process.env.FIREBASE_CREDENTIALS_FILE;
|
|
23
|
+
if (!file) {
|
|
24
|
+
// Sem ADC e sem arquivo: inicializa via ADC mesmo assim — em
|
|
25
|
+
// ambientes GCP funciona; fora, falha alto na primeira chamada
|
|
26
|
+
// (melhor que embutir credencial).
|
|
27
|
+
admin.initializeApp();
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const serviceAccount = require(file);
|
|
31
|
+
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
adminInstance = admin;
|
|
36
|
+
return admin;
|
|
37
|
+
}
|
|
38
|
+
// --- helper multi-tenant cacheado (Firebase Auth: 1 tenant por escola) ---
|
|
39
|
+
const tenantAuthCache = new Map();
|
|
40
|
+
function getTenantAuth(tenantId) {
|
|
41
|
+
if (!tenantId)
|
|
42
|
+
throw new Error("Token sem claim de tenant Firebase");
|
|
43
|
+
let a = tenantAuthCache.get(tenantId);
|
|
44
|
+
if (!a) {
|
|
45
|
+
a = initFirebaseAdmin().auth().tenantManager().authForTenant(tenantId);
|
|
46
|
+
tenantAuthCache.set(tenantId, a);
|
|
47
|
+
}
|
|
48
|
+
return a;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/firebase/admin.ts"],"names":[],"mappings":";AAAA,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAC7E,wDAAwD;;AAUxD,8CA2BC;AAKD,sCAQC;AAhDD,IAAI,aAAa,GAAQ,IAAI,CAAC;AAQ9B,SAAgB,iBAAiB,CAAC,UAAgC,EAAE;IAChE,IAAI,aAAa;QAAE,OAAO,aAAa,CAAC;IACxC,uDAAuD;IACvD,MAAM,KAAK,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAExC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACrB,MAAM,MAAM,GACR,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,MAAM;YAC9B,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC;QAEjD,IAAI,MAAM,EAAE,CAAC;YACT,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,8BAA8B;QACzD,CAAC;aAAM,CAAC;YACJ,MAAM,IAAI,GAAG,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;YAC9E,IAAI,CAAC,IAAI,EAAE,CAAC;gBACR,6DAA6D;gBAC7D,+DAA+D;gBAC/D,mCAAmC;gBACnC,KAAK,CAAC,aAAa,EAAE,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACJ,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;gBACrC,KAAK,CAAC,aAAa,CAAC,EAAC,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,cAAc,CAAC,EAAC,CAAC,CAAC;YAC7E,CAAC;QACL,CAAC;IACL,CAAC;IACD,aAAa,GAAG,KAAK,CAAC;IACtB,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,4EAA4E;AAC5E,MAAM,eAAe,GAAG,IAAI,GAAG,EAAe,CAAC;AAE/C,SAAgB,aAAa,CAAC,QAA4B;IACtD,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACrE,IAAI,CAAC,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,CAAC,CAAC,EAAE,CAAC;QACL,CAAC,GAAG,iBAAiB,EAAE,CAAC,IAAI,EAAE,CAAC,aAAa,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACvE,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,CAAC,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Firebase CLIENT SDK (storage) das APIs. Config 100% via env — as chaves de
|
|
3
|
+
// client do Firebase (apiKey etc.) não são segredo, mas nada fica hardcoded.
|
|
4
|
+
// `firebase` é peer dep; injeção opcional dos módulos para teste.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createFirebaseClientPlugin = createFirebaseClientPlugin;
|
|
7
|
+
function createFirebaseClientPlugin(options = {}) {
|
|
8
|
+
return async function pluginFirebase(fastify) {
|
|
9
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
10
|
+
const initializeApp = options.initializeApp ?? require("firebase/app").initializeApp;
|
|
11
|
+
const getStorage = options.getStorage ?? require("firebase/storage").getStorage;
|
|
12
|
+
const firebaseConfig = {
|
|
13
|
+
apiKey: process.env.APIKEY,
|
|
14
|
+
authDomain: process.env.AUTHDOMAIN,
|
|
15
|
+
projectId: process.env.PROJECTID,
|
|
16
|
+
storageBucket: process.env.STORAGEBUCKET,
|
|
17
|
+
messagingSenderId: process.env.MESSAGINGSENDERID,
|
|
18
|
+
appId: process.env.APPID,
|
|
19
|
+
measurementId: process.env.MEASUREMENTID,
|
|
20
|
+
};
|
|
21
|
+
const appFirebase = initializeApp(firebaseConfig);
|
|
22
|
+
const storageFirebase = getStorage(appFirebase);
|
|
23
|
+
fastify.decorate("firebase", appFirebase);
|
|
24
|
+
fastify.decorate("storage", storageFirebase);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=client-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-plugin.js","sourceRoot":"","sources":["../../src/firebase/client-plugin.ts"],"names":[],"mappings":";AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,kEAAkE;;AAOlE,gEAoBC;AApBD,SAAgB,0BAA0B,CAAC,UAAuC,EAAE;IAChF,OAAO,KAAK,UAAU,cAAc,CAAC,OAAY;QAC7C,uDAAuD;QACvD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC,aAAa,CAAC;QACrF,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC,UAAU,CAAC;QAEhF,MAAM,cAAc,GAAG;YACnB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM;YAC1B,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU;YAClC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS;YAChC,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;YACxC,iBAAiB,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB;YAChD,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK;YACxB,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;SAC3C,CAAC;QACF,MAAM,WAAW,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC;QAClD,MAAM,eAAe,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;QAChD,OAAO,CAAC,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IACjD,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ErrorHandlerOptions {
|
|
2
|
+
/** Mensagem genérica para erros internos (default pt-BR) */
|
|
3
|
+
genericMessage?: string;
|
|
4
|
+
/** Força expor mensagens cruas (ex: dev local). Default: false */
|
|
5
|
+
exposeInternal?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function isInternalError(error: any): boolean;
|
|
8
|
+
export declare function createErrorHandler(options?: ErrorHandlerOptions): (fastify: any) => Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Error handler padrão das APIs. Corrige o vazamento de internals: erros de
|
|
3
|
+
// infra (Prisma/banco/rede) NUNCA chegam ao cliente com a mensagem crua —
|
|
4
|
+
// "Can't reach database server at /cloudsql/..." no browser do aluno é
|
|
5
|
+
// exposição de infraestrutura. Log completo sempre; resposta sanitizada.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.isInternalError = isInternalError;
|
|
8
|
+
exports.createErrorHandler = createErrorHandler;
|
|
9
|
+
// Padrões que denunciam erro de infraestrutura — nunca expor ao cliente.
|
|
10
|
+
const INTERNAL_PATTERNS = /prisma|cloudsql|can't reach database|pool timeout|econnrefused|etimedout|enotfound|getaddrinfo|socket|connection/i;
|
|
11
|
+
function isInternalError(error) {
|
|
12
|
+
if (error?.name?.startsWith?.("PrismaClient"))
|
|
13
|
+
return true;
|
|
14
|
+
if (typeof error?.code === "string" && /^P\d{4}$/.test(error.code))
|
|
15
|
+
return true;
|
|
16
|
+
if ((error?.statusCode ?? 0) >= 500)
|
|
17
|
+
return true;
|
|
18
|
+
return INTERNAL_PATTERNS.test(String(error?.message ?? ""));
|
|
19
|
+
}
|
|
20
|
+
function createErrorHandler(options = {}) {
|
|
21
|
+
const generic = options.genericMessage ?? "Erro interno no processamento";
|
|
22
|
+
return async function onErrorMiddleware(fastify) {
|
|
23
|
+
fastify.setErrorHandler(async (error, request, reply) => {
|
|
24
|
+
console.error(error);
|
|
25
|
+
const expose = options.exposeInternal || !isInternalError(error);
|
|
26
|
+
reply.code(400).send({
|
|
27
|
+
statusCode: 400,
|
|
28
|
+
message: expose ? error.message : generic,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=error-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/http/error-handler.ts"],"names":[],"mappings":";AAAA,4EAA4E;AAC5E,0EAA0E;AAC1E,uEAAuE;AACvE,yEAAyE;;AAYzE,0CAKC;AAED,gDAYC;AAtBD,yEAAyE;AACzE,MAAM,iBAAiB,GAAG,mHAAmH,CAAC;AAE9I,SAAgB,eAAe,CAAC,KAAU;IACtC,IAAI,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,cAAc,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,IAAI,OAAO,KAAK,EAAE,IAAI,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAChF,IAAI,CAAC,KAAK,EAAE,UAAU,IAAI,CAAC,CAAC,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,SAAgB,kBAAkB,CAAC,UAA+B,EAAE;IAChE,MAAM,OAAO,GAAG,OAAO,CAAC,cAAc,IAAI,+BAA+B,CAAC;IAC1E,OAAO,KAAK,UAAU,iBAAiB,CAAC,OAAY;QAChD,OAAO,CAAC,eAAe,CAAC,KAAK,EAAE,KAAU,EAAE,OAAY,EAAE,KAAU,EAAE,EAAE;YACnE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,MAAM,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YACjE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACjB,UAAU,EAAE,GAAG;gBACf,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;aAC5C,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;AACN,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { resolveTenantId } from "./tenant/resolve";
|
|
2
|
+
export type { TenantRequestLike } from "./tenant/resolve";
|
|
3
|
+
export { createTenantMiddleware } from "./tenant/middleware";
|
|
4
|
+
export type { TenantMiddlewareOptions } from "./tenant/middleware";
|
|
5
|
+
export { createAuthMiddleware } from "./tenant/auth";
|
|
6
|
+
export type { AuthMiddlewareOptions, TenantAuthLike } from "./tenant/auth";
|
|
7
|
+
export { createMultitenantPrisma } from "./prisma/multitenant";
|
|
8
|
+
export { createMultitenantPrismaMariaDb } from "./prisma/mariadb-factory";
|
|
9
|
+
export type { MariaDbMultitenantOptions } from "./prisma/mariadb-factory";
|
|
10
|
+
export type { TenantClient, Multitenant, MultitenantOptions, PoolConfigLike, PoolLike, } from "./prisma/multitenant";
|
|
11
|
+
export { createCorsPlugin, DEFAULT_CORS } from "./cors/plugin";
|
|
12
|
+
export type { CorsOptions } from "./cors/plugin";
|
|
13
|
+
export { createSwaggerPlugin } from "./swagger/plugin";
|
|
14
|
+
export type { SwaggerPluginOptions } from "./swagger/plugin";
|
|
15
|
+
export { createFirebaseClientPlugin } from "./firebase/client-plugin";
|
|
16
|
+
export type { FirebaseClientPluginOptions } from "./firebase/client-plugin";
|
|
17
|
+
export { initFirebaseAdmin, getTenantAuth } from "./firebase/admin";
|
|
18
|
+
export type { FirebaseAdminOptions } from "./firebase/admin";
|
|
19
|
+
export { createErrorHandler, isInternalError } from "./http/error-handler";
|
|
20
|
+
export type { ErrorHandlerOptions } from "./http/error-handler";
|
|
21
|
+
export { GeminiHelp, EMBED_DIM, GEMINI_MODEL, EMBED_MODEL } from "./ai/gemini";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMBED_MODEL = exports.GEMINI_MODEL = exports.EMBED_DIM = exports.GeminiHelp = exports.isInternalError = exports.createErrorHandler = exports.getTenantAuth = exports.initFirebaseAdmin = exports.createFirebaseClientPlugin = exports.createSwaggerPlugin = exports.DEFAULT_CORS = exports.createCorsPlugin = exports.createMultitenantPrismaMariaDb = exports.createMultitenantPrisma = exports.createAuthMiddleware = exports.createTenantMiddleware = exports.resolveTenantId = void 0;
|
|
4
|
+
var resolve_1 = require("./tenant/resolve");
|
|
5
|
+
Object.defineProperty(exports, "resolveTenantId", { enumerable: true, get: function () { return resolve_1.resolveTenantId; } });
|
|
6
|
+
var middleware_1 = require("./tenant/middleware");
|
|
7
|
+
Object.defineProperty(exports, "createTenantMiddleware", { enumerable: true, get: function () { return middleware_1.createTenantMiddleware; } });
|
|
8
|
+
var auth_1 = require("./tenant/auth");
|
|
9
|
+
Object.defineProperty(exports, "createAuthMiddleware", { enumerable: true, get: function () { return auth_1.createAuthMiddleware; } });
|
|
10
|
+
var multitenant_1 = require("./prisma/multitenant");
|
|
11
|
+
Object.defineProperty(exports, "createMultitenantPrisma", { enumerable: true, get: function () { return multitenant_1.createMultitenantPrisma; } });
|
|
12
|
+
var mariadb_factory_1 = require("./prisma/mariadb-factory");
|
|
13
|
+
Object.defineProperty(exports, "createMultitenantPrismaMariaDb", { enumerable: true, get: function () { return mariadb_factory_1.createMultitenantPrismaMariaDb; } });
|
|
14
|
+
var plugin_1 = require("./cors/plugin");
|
|
15
|
+
Object.defineProperty(exports, "createCorsPlugin", { enumerable: true, get: function () { return plugin_1.createCorsPlugin; } });
|
|
16
|
+
Object.defineProperty(exports, "DEFAULT_CORS", { enumerable: true, get: function () { return plugin_1.DEFAULT_CORS; } });
|
|
17
|
+
var plugin_2 = require("./swagger/plugin");
|
|
18
|
+
Object.defineProperty(exports, "createSwaggerPlugin", { enumerable: true, get: function () { return plugin_2.createSwaggerPlugin; } });
|
|
19
|
+
var client_plugin_1 = require("./firebase/client-plugin");
|
|
20
|
+
Object.defineProperty(exports, "createFirebaseClientPlugin", { enumerable: true, get: function () { return client_plugin_1.createFirebaseClientPlugin; } });
|
|
21
|
+
var admin_1 = require("./firebase/admin");
|
|
22
|
+
Object.defineProperty(exports, "initFirebaseAdmin", { enumerable: true, get: function () { return admin_1.initFirebaseAdmin; } });
|
|
23
|
+
Object.defineProperty(exports, "getTenantAuth", { enumerable: true, get: function () { return admin_1.getTenantAuth; } });
|
|
24
|
+
var error_handler_1 = require("./http/error-handler");
|
|
25
|
+
Object.defineProperty(exports, "createErrorHandler", { enumerable: true, get: function () { return error_handler_1.createErrorHandler; } });
|
|
26
|
+
Object.defineProperty(exports, "isInternalError", { enumerable: true, get: function () { return error_handler_1.isInternalError; } });
|
|
27
|
+
var gemini_1 = require("./ai/gemini");
|
|
28
|
+
Object.defineProperty(exports, "GeminiHelp", { enumerable: true, get: function () { return gemini_1.GeminiHelp; } });
|
|
29
|
+
Object.defineProperty(exports, "EMBED_DIM", { enumerable: true, get: function () { return gemini_1.EMBED_DIM; } });
|
|
30
|
+
Object.defineProperty(exports, "GEMINI_MODEL", { enumerable: true, get: function () { return gemini_1.GEMINI_MODEL; } });
|
|
31
|
+
Object.defineProperty(exports, "EMBED_MODEL", { enumerable: true, get: function () { return gemini_1.EMBED_MODEL; } });
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,4CAAiD;AAAzC,0GAAA,eAAe,OAAA;AAGvB,kDAA2D;AAAnD,oHAAA,sBAAsB,OAAA;AAG9B,sCAAmD;AAA3C,4GAAA,oBAAoB,OAAA;AAG5B,oDAA6D;AAArD,sHAAA,uBAAuB,OAAA;AAC/B,4DAAwE;AAAhE,iIAAA,8BAA8B,OAAA;AAUtC,wCAA6D;AAArD,0GAAA,gBAAgB,OAAA;AAAE,sGAAA,YAAY,OAAA;AAGtC,2CAAqD;AAA7C,6GAAA,mBAAmB,OAAA;AAG3B,0DAAoE;AAA5D,2HAAA,0BAA0B,OAAA;AAGlC,0CAAkE;AAA1D,0GAAA,iBAAiB,OAAA;AAAE,sGAAA,aAAa,OAAA;AAGxC,sDAAyE;AAAjE,mHAAA,kBAAkB,OAAA;AAAE,gHAAA,eAAe,OAAA;AAG3C,sCAA6E;AAArE,oGAAA,UAAU,OAAA;AAAE,mGAAA,SAAS,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,qGAAA,WAAW,OAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import mariadb from "mariadb";
|
|
2
|
+
import { Multitenant, MultitenantOptions } from "./multitenant";
|
|
3
|
+
export type MariaDbMultitenantOptions<TClient> = Omit<MultitenantOptions<TClient, mariadb.Pool>, "PrismaMariaDb" | "createPool">;
|
|
4
|
+
export declare function createMultitenantPrismaMariaDb<TClient>(options: MariaDbMultitenantOptions<TClient>): Multitenant<TClient>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createMultitenantPrismaMariaDb = createMultitenantPrismaMariaDb;
|
|
7
|
+
// Fábrica "baterias inclusas": o pkg é o dono das versões do driver mariadb e
|
|
8
|
+
// do @prisma/adapter-mariadb (fonte única — as APIs não declaram essas libs).
|
|
9
|
+
// A API só injeta o PrismaClient GERADO dela.
|
|
10
|
+
const mariadb_1 = __importDefault(require("mariadb"));
|
|
11
|
+
const adapter_mariadb_1 = require("@prisma/adapter-mariadb");
|
|
12
|
+
const multitenant_1 = require("./multitenant");
|
|
13
|
+
function createMultitenantPrismaMariaDb(options) {
|
|
14
|
+
return (0, multitenant_1.createMultitenantPrisma)({
|
|
15
|
+
...options,
|
|
16
|
+
PrismaMariaDb: adapter_mariadb_1.PrismaMariaDb,
|
|
17
|
+
createPool: mariadb_1.default.createPool,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=mariadb-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-factory.js","sourceRoot":"","sources":["../../src/prisma/mariadb-factory.ts"],"names":[],"mappings":";;;;;AAYA,wEAQC;AApBD,8EAA8E;AAC9E,8EAA8E;AAC9E,8CAA8C;AAC9C,sDAA8B;AAC9B,6DAAsD;AACtD,+CAAuF;AAOvF,SAAgB,8BAA8B,CAC1C,OAA2C;IAE3C,OAAO,IAAA,qCAAuB,EAAwB;QAClD,GAAG,OAAO;QACV,aAAa,EAAE,+BAAoB;QACnC,UAAU,EAAE,iBAAO,CAAC,UAAU;KACjC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type TenantClient<T> = T & {
|
|
2
|
+
$schema: string;
|
|
3
|
+
};
|
|
4
|
+
export interface PoolConfigLike {
|
|
5
|
+
host?: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
socketPath?: string;
|
|
8
|
+
user?: string;
|
|
9
|
+
password?: string;
|
|
10
|
+
connectionLimit?: number;
|
|
11
|
+
connectTimeout?: number;
|
|
12
|
+
acquireTimeout?: number;
|
|
13
|
+
prepareCacheLength?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface PoolLike {
|
|
16
|
+
end(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface MultitenantOptions<TClient, TPool extends PoolLike> {
|
|
19
|
+
/** DATABASE_URL no formato mysql://user:pass@host:porta/db_school_1?connection_limit=N
|
|
20
|
+
* ou, em prod, mysql://user:pass@localhost/db_school_1?socket=/cloudsql/proj:region:inst */
|
|
21
|
+
databaseUrl: string;
|
|
22
|
+
/** Construtor do PrismaClient GERADO da API (src/config/prisma/generated).
|
|
23
|
+
* args tipado como any de propósito: o construtor real exige o tipo
|
|
24
|
+
* concreto do adapter (SqlDriverAdapterFactory) e a contravariância de
|
|
25
|
+
* parâmetros rejeitaria unknown. */
|
|
26
|
+
PrismaClient: new (args: any) => TClient;
|
|
27
|
+
/** Construtor PrismaMariaDb de @prisma/adapter-mariadb */
|
|
28
|
+
PrismaMariaDb: new (pool: TPool, opts?: {
|
|
29
|
+
database?: string;
|
|
30
|
+
}) => unknown;
|
|
31
|
+
/** mariadb.createPool do driver mariadb */
|
|
32
|
+
createPool: (config: PoolConfigLike) => TPool;
|
|
33
|
+
/** Limite de conexões do pool (default: connection_limit da URL, PRISMA_POOL_LIMIT, ou 5) */
|
|
34
|
+
poolLimit?: number;
|
|
35
|
+
connectTimeout?: number;
|
|
36
|
+
acquireTimeout?: number;
|
|
37
|
+
/** Deriva o nome do database do tenant (default: troca "school_1" por "school_{id}") */
|
|
38
|
+
tenantDbName?: (baseDbName: string, idSchool: string) => string;
|
|
39
|
+
log?: unknown;
|
|
40
|
+
errorFormat?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface Multitenant<TClient> {
|
|
43
|
+
pool: PoolLike;
|
|
44
|
+
/** Client do db "base" (o da própria DATABASE_URL) */
|
|
45
|
+
defaultClient: TenantClient<TClient>;
|
|
46
|
+
/** Client leve do tenant; id inválido cai no defaultClient */
|
|
47
|
+
getTenantClient: (idSchool: string) => TenantClient<TClient>;
|
|
48
|
+
/** Nome do database para um tenant (para raws construídos fora do client) */
|
|
49
|
+
dbNameFor: (idSchool: string) => string;
|
|
50
|
+
/** Desconecta todos os clients e encerra o pool (shutdown) */
|
|
51
|
+
disconnectAll: () => Promise<void>;
|
|
52
|
+
/** Plugin Fastify: decora fastify.defaultPrisma e desconecta tudo no onClose */
|
|
53
|
+
plugin: (fastify: any) => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export declare function createMultitenantPrisma<TClient, TPool extends PoolLike>(options: MultitenantOptions<TClient, TPool>): Multitenant<TClient>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Multitenancy Prisma 7 com POOL ÚNICO compartilhado entre todas as escolas.
|
|
3
|
+
// O client de cada tenant é um objeto leve cujo adapter apenas qualifica o
|
|
4
|
+
// database no SQL gerado (`school_N`.`tabela`) — trocar de escola NÃO abre
|
|
5
|
+
// conexão nova. O pool é criado SEM database default: raw sem qualificação
|
|
6
|
+
// falha alto (Error 1046) em vez de cair silenciosamente no tenant errado.
|
|
7
|
+
//
|
|
8
|
+
// A lib não depende de @prisma/client, @prisma/adapter-mariadb nem mariadb:
|
|
9
|
+
// cada API injeta o SEU client gerado e as suas versões do adapter/driver.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.createMultitenantPrisma = createMultitenantPrisma;
|
|
12
|
+
function defaultTenantDbName(baseDbName, idSchool) {
|
|
13
|
+
return baseDbName.replace("school_1", `school_${idSchool}`);
|
|
14
|
+
}
|
|
15
|
+
function createMultitenantPrisma(options) {
|
|
16
|
+
const parsed = new URL(options.databaseUrl);
|
|
17
|
+
const socketPath = parsed.searchParams.get("socket") || undefined;
|
|
18
|
+
const baseDbName = parsed.pathname.replace(/^\//, "").split("?")[0];
|
|
19
|
+
const poolLimit = options.poolLimit ??
|
|
20
|
+
Number(process.env.PRISMA_POOL_LIMIT ||
|
|
21
|
+
parsed.searchParams.get("connection_limit") ||
|
|
22
|
+
5);
|
|
23
|
+
const pool = options.createPool({
|
|
24
|
+
...(socketPath
|
|
25
|
+
? { socketPath }
|
|
26
|
+
: { host: parsed.hostname, port: Number(parsed.port || 3306) }),
|
|
27
|
+
user: decodeURIComponent(parsed.username),
|
|
28
|
+
password: decodeURIComponent(parsed.password),
|
|
29
|
+
connectionLimit: poolLimit,
|
|
30
|
+
connectTimeout: options.connectTimeout ?? 15000,
|
|
31
|
+
acquireTimeout: options.acquireTimeout ?? 20000,
|
|
32
|
+
// workaround do leak de prepared statements com pool externo
|
|
33
|
+
// (https://github.com/prisma/prisma/issues/29364)
|
|
34
|
+
prepareCacheLength: 0,
|
|
35
|
+
});
|
|
36
|
+
const tenantDbName = options.tenantDbName ?? defaultTenantDbName;
|
|
37
|
+
const clients = new Map();
|
|
38
|
+
function clientFor(dbName) {
|
|
39
|
+
let client = clients.get(dbName);
|
|
40
|
+
if (!client) {
|
|
41
|
+
const adapter = new options.PrismaMariaDb(pool, { database: dbName });
|
|
42
|
+
client = new options.PrismaClient({
|
|
43
|
+
adapter,
|
|
44
|
+
log: options.log,
|
|
45
|
+
errorFormat: options.errorFormat,
|
|
46
|
+
});
|
|
47
|
+
client.$schema = dbName;
|
|
48
|
+
clients.set(dbName, client);
|
|
49
|
+
}
|
|
50
|
+
return client;
|
|
51
|
+
}
|
|
52
|
+
const defaultClient = clientFor(baseDbName);
|
|
53
|
+
function dbNameFor(idSchool) {
|
|
54
|
+
if (!/^\d+$/.test(idSchool) || Number(idSchool) <= 0)
|
|
55
|
+
return baseDbName;
|
|
56
|
+
return tenantDbName(baseDbName, idSchool);
|
|
57
|
+
}
|
|
58
|
+
function getTenantClient(idSchool) {
|
|
59
|
+
// defesa extra: o nome entra em SQL qualificado, só aceita dígitos
|
|
60
|
+
if (!/^\d+$/.test(idSchool) || Number(idSchool) <= 0)
|
|
61
|
+
return defaultClient;
|
|
62
|
+
return clientFor(tenantDbName(baseDbName, idSchool));
|
|
63
|
+
}
|
|
64
|
+
async function disconnectAll() {
|
|
65
|
+
await Promise.all(Array.from(clients.values()).map(c => c.$disconnect?.().catch(() => {
|
|
66
|
+
})));
|
|
67
|
+
await pool.end().catch(() => {
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const plugin = async (fastify) => {
|
|
71
|
+
fastify.decorate("defaultPrisma", defaultClient);
|
|
72
|
+
fastify.addHook("onClose", disconnectAll);
|
|
73
|
+
};
|
|
74
|
+
return { pool, defaultClient, getTenantClient, dbNameFor, disconnectAll, plugin };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=multitenant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multitenant.js","sourceRoot":"","sources":["../../src/prisma/multitenant.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Swagger padrão das APIs MyBuilder. A API só informa title/description
|
|
3
|
+
// (e opcionalmente version/routePrefix); o resto da configuração é único.
|
|
4
|
+
// @fastify/swagger e @fastify/swagger-ui são peer deps (as APIs já têm).
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createSwaggerPlugin = createSwaggerPlugin;
|
|
7
|
+
// BigInt não serializa em JSON — os CRUDs devolvem ids BigInt do Prisma.
|
|
8
|
+
// Patch global (efeito colateral consciente, documentado no README).
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
if (!BigInt.prototype.toJSON) {
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
BigInt.prototype.toJSON = function () {
|
|
13
|
+
const int = Number.parseInt(this.toString());
|
|
14
|
+
return int ?? this.toString();
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function createSwaggerPlugin(options) {
|
|
18
|
+
return async function pluginSwagger(fastify) {
|
|
19
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
20
|
+
const Swagger = require("@fastify/swagger");
|
|
21
|
+
const SwaggerUi = require("@fastify/swagger-ui");
|
|
22
|
+
await fastify.register(Swagger, {
|
|
23
|
+
staticCSP: true,
|
|
24
|
+
exposeRoute: true,
|
|
25
|
+
swagger: {
|
|
26
|
+
info: {
|
|
27
|
+
title: options.title,
|
|
28
|
+
description: options.description,
|
|
29
|
+
version: options.version ?? "0.1.0",
|
|
30
|
+
},
|
|
31
|
+
host: process.env.DOMAIN,
|
|
32
|
+
schemes: [process.env.HTTP],
|
|
33
|
+
securityDefinitions: {
|
|
34
|
+
bearerAuth: {
|
|
35
|
+
type: "apiKey",
|
|
36
|
+
name: "Authorization",
|
|
37
|
+
in: "header",
|
|
38
|
+
description: "Insira o token Firebase JWT no formato: Bearer {token}",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
security: [{ bearerAuth: [] }],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
await fastify.register(SwaggerUi, {
|
|
45
|
+
routePrefix: options.routePrefix ?? "/documentation",
|
|
46
|
+
uiConfig: {
|
|
47
|
+
docExpansion: "none",
|
|
48
|
+
deepLinking: false,
|
|
49
|
+
},
|
|
50
|
+
staticCSP: true,
|
|
51
|
+
exposeRoute: true,
|
|
52
|
+
transformStaticCSP: (header) => header,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/swagger/plugin.ts"],"names":[],"mappings":";AAAA,wEAAwE;AACxE,0EAA0E;AAC1E,yEAAyE;;AAoBzE,kDAwCC;AAnDD,yEAAyE;AACzE,qEAAqE;AACrE,aAAa;AACb,IAAI,CAAE,MAAM,CAAC,SAAiB,CAAC,MAAM,EAAE,CAAC;IACpC,aAAa;IACb,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG;QACtB,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC7C,OAAO,GAAG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;IAClC,CAAC,CAAC;AACN,CAAC;AAED,SAAgB,mBAAmB,CAAC,OAA6B;IAC7D,OAAO,KAAK,UAAU,aAAa,CAAC,OAAY;QAC5C,uDAAuD;QACvD,MAAM,OAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;QAEjD,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE;YAC5B,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,IAAI;YACjB,OAAO,EAAE;gBACL,IAAI,EAAE;oBACF,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,OAAO;iBACtC;gBACD,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM;gBACxB,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC3B,mBAAmB,EAAE;oBACjB,UAAU,EAAE;wBACR,IAAI,EAAE,QAAQ;wBACd,IAAI,EAAE,eAAe;wBACrB,EAAE,EAAE,QAAQ;wBACZ,WAAW,EAAE,wDAAwD;qBACxE;iBACJ;gBACD,QAAQ,EAAE,CAAC,EAAC,UAAU,EAAE,EAAE,EAAC,CAAC;aAC/B;SACJ,CAAC,CAAC;QAEH,MAAM,OAAO,CAAC,QAAQ,CAAC,SAAS,EAAE;YAC9B,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,gBAAgB;YACpD,QAAQ,EAAE;gBACN,YAAY,EAAE,MAAM;gBACpB,WAAW,EAAE,KAAK;aACrB;YACD,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,IAAI;YACjB,kBAAkB,EAAE,CAAC,MAAW,EAAE,EAAE,CAAC,MAAM;SAC9C,CAAC,CAAC;IACP,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface TenantAuthLike {
|
|
2
|
+
verifyIdToken(token: string, checkRevoked?: boolean): Promise<any>;
|
|
3
|
+
}
|
|
4
|
+
export interface AuthMiddlewareOptions {
|
|
5
|
+
/** Rotas públicas (regex sobre a URL crua) — dispensam token */
|
|
6
|
+
publicRoutes: RegExp[];
|
|
7
|
+
/** Resolve o Auth do tenant Firebase (ex: getTenantAuth do pkg) */
|
|
8
|
+
getTenantAuth: (tenantId: string | undefined) => TenantAuthLike;
|
|
9
|
+
/** Aceitar token via ?token= (default: false — vaza em logs) */
|
|
10
|
+
allowQueryToken?: boolean;
|
|
11
|
+
/** Bypass SEM token (dev local). NUNCA ligar em produção. Default: false */
|
|
12
|
+
devBypass?: boolean;
|
|
13
|
+
/** Vínculo tenant↔recurso: lança para negar (vira 403). Recebe o token
|
|
14
|
+
* decodificado e o request (com params/query/headers). */
|
|
15
|
+
assertTenant?: (decodedToken: any, request: any) => void | Promise<void>;
|
|
16
|
+
/** TTL máximo do cache de tokens verificados, em ms (default 120s) */
|
|
17
|
+
cacheTtlMs?: number;
|
|
18
|
+
/** Tamanho máximo do cache (default 2000) */
|
|
19
|
+
cacheMaxSize?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function createAuthMiddleware(options: AuthMiddlewareOptions): (fastify: any) => Promise<void>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Autenticação Firebase multi-tenant — fonte única e testada.
|
|
3
|
+
//
|
|
4
|
+
// Correções de segurança em relação ao middleware original das APIs:
|
|
5
|
+
// 1. SEM bypass por hostname: "localhost" vinha do header Host (controlável
|
|
6
|
+
// pelo cliente) — bypass de dev agora é OPT-IN explícito via option
|
|
7
|
+
// `devBypass` (ligado por env da API, morto em produção por padrão).
|
|
8
|
+
// 2. Token via query string (?token=) é OPT-IN (`allowQueryToken`) — URLs
|
|
9
|
+
// vazam em logs/histórico; só habilitar para fluxos que precisam (mídia).
|
|
10
|
+
// 3. Gancho `assertTenant` para amarrar o tenant do token ao id_school da
|
|
11
|
+
// URL (falha => 403) — o isolamento de dados exige esse vínculo.
|
|
12
|
+
//
|
|
13
|
+
// O tenant Firebase vem do claim `firebase.tenant` do próprio token; a
|
|
14
|
+
// VALIDAÇÃO criptográfica é do verifyIdToken do tenant correspondente —
|
|
15
|
+
// um token forjado para outro tenant não verifica.
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|
|
18
|
+
function createAuthMiddleware(options) {
|
|
19
|
+
const TOKEN_CACHE = new Map();
|
|
20
|
+
const MAX_CACHE_SIZE = options.cacheMaxSize ?? 2000;
|
|
21
|
+
const MAX_TTL_MS = options.cacheTtlMs ?? 120 * 1000;
|
|
22
|
+
function getCached(token) {
|
|
23
|
+
const hit = TOKEN_CACHE.get(token);
|
|
24
|
+
if (!hit)
|
|
25
|
+
return null;
|
|
26
|
+
if (hit.expMs <= Date.now()) {
|
|
27
|
+
TOKEN_CACHE.delete(token);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return hit.user;
|
|
31
|
+
}
|
|
32
|
+
function setCached(token, user, expSeconds) {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
const expMs = Math.min(expSeconds * 1000, now + MAX_TTL_MS);
|
|
35
|
+
TOKEN_CACHE.set(token, { expMs, user });
|
|
36
|
+
if (TOKEN_CACHE.size > MAX_CACHE_SIZE) {
|
|
37
|
+
const it = TOKEN_CACHE.keys();
|
|
38
|
+
for (let i = 0; i < 100 && TOKEN_CACHE.size > MAX_CACHE_SIZE; i++) {
|
|
39
|
+
const k = it.next().value;
|
|
40
|
+
if (!k)
|
|
41
|
+
break;
|
|
42
|
+
TOKEN_CACHE.delete(k);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return async function authMiddleware(fastify) {
|
|
47
|
+
fastify.decorate("authenticate", async (request, reply) => {
|
|
48
|
+
try {
|
|
49
|
+
const url = request.raw?.url || "";
|
|
50
|
+
if (options.publicRoutes.some(regex => regex.test(url)) || url === "" || url === "/") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let authHeader = request.headers?.authorization;
|
|
54
|
+
if (options.allowQueryToken && request.query?.token) {
|
|
55
|
+
authHeader = request.query.token;
|
|
56
|
+
}
|
|
57
|
+
if (!authHeader) {
|
|
58
|
+
// bypass de dev: só por opção explícita, nunca por hostname
|
|
59
|
+
if (options.devBypass)
|
|
60
|
+
return;
|
|
61
|
+
return reply.status(401).send({ message: "Token ausente ou inválido" });
|
|
62
|
+
}
|
|
63
|
+
authHeader = authHeader.replace("Bearer ", "");
|
|
64
|
+
const parts = authHeader.split(".");
|
|
65
|
+
if (parts.length !== 3) {
|
|
66
|
+
return reply.status(401).send({ message: "Token inválido" });
|
|
67
|
+
}
|
|
68
|
+
const cached = getCached(authHeader);
|
|
69
|
+
if (cached) {
|
|
70
|
+
if (options.assertTenant) {
|
|
71
|
+
try {
|
|
72
|
+
await options.assertTenant(cached, request);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return reply.status(403).send({ message: "Acesso negado para esta escola" });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
request.user = cached;
|
|
79
|
+
request.authToken = authHeader;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// o claim de tenant orienta QUAL Auth valida; a prova é criptográfica
|
|
83
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8"));
|
|
84
|
+
const tenantId = payload?.firebase?.tenant;
|
|
85
|
+
const auth = options.getTenantAuth(tenantId);
|
|
86
|
+
const decodedToken = await auth.verifyIdToken(authHeader, true);
|
|
87
|
+
setCached(authHeader, decodedToken, decodedToken.exp);
|
|
88
|
+
if (options.assertTenant) {
|
|
89
|
+
try {
|
|
90
|
+
await options.assertTenant(decodedToken, request);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return reply.status(403).send({ message: "Acesso negado para esta escola" });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
request.user = decodedToken;
|
|
97
|
+
request.authToken = authHeader;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.error("Authentication error:", err);
|
|
101
|
+
return reply.status(401).send({ message: "Token inválido" });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
fastify.addHook("onRequest", fastify.authenticate);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/tenant/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Middleware de tenant: injeta req.prisma com o client da escola do request
|
|
3
|
+
// (params.id_school -> query.id_school -> header x-id-school), com fallback
|
|
4
|
+
// para o client default quando não há id válido. É a ÚNICA porta de entrada
|
|
5
|
+
// oficial do Prisma nos handlers — garante que o :id_school da URL decide o
|
|
6
|
+
// schema, nunca o handler.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createTenantMiddleware = createTenantMiddleware;
|
|
9
|
+
const resolve_1 = require("./resolve");
|
|
10
|
+
function createTenantMiddleware(options) {
|
|
11
|
+
return async function tenantMiddleware(fastify) {
|
|
12
|
+
fastify.addHook("preValidation", async (req) => {
|
|
13
|
+
const idSchool = (0, resolve_1.resolveTenantId)(req);
|
|
14
|
+
req.prisma = idSchool
|
|
15
|
+
? options.getTenantClient(idSchool)
|
|
16
|
+
: options.defaultClient;
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +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;;AAS3B,wDASC;AAhBD,uCAA6D;AAO7D,SAAgB,sBAAsB,CAAU,OAAyC;IACrF,OAAO,KAAK,UAAU,gBAAgB,CAAC,OAAY;QAC/C,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,EAAE,GAA6C,EAAE,EAAE;YACrF,MAAM,QAAQ,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAC;YACtC,GAAG,CAAC,MAAM,GAAG,QAAQ;gBACjB,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC;gBACnC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;QAChC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Resolução do tenant a partir do request.
|
|
3
|
+
// Ordem oficial (a mesma das APIs desde sempre): params.id_school -> query.id_school -> header x-id-school.
|
|
4
|
+
// Só aceita inteiro positivo em string de dígitos — qualquer outra coisa é
|
|
5
|
+
// tratada como "sem tenant" (o chamador decide o fallback), nunca interpolada em SQL.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.resolveTenantId = resolveTenantId;
|
|
8
|
+
function resolveTenantId(req) {
|
|
9
|
+
const raw = req.params?.id_school ??
|
|
10
|
+
req.query?.id_school ??
|
|
11
|
+
req.headers?.["x-id-school"];
|
|
12
|
+
const idStr = typeof raw === "string" ? raw : raw != null ? String(raw) : "";
|
|
13
|
+
if (idStr && /^\d+$/.test(idStr) && Number(idStr) > 0)
|
|
14
|
+
return idStr;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/tenant/resolve.ts"],"names":[],"mappings":";AAAA,2CAA2C;AAC3C,4GAA4G;AAC5G,2EAA2E;AAC3E,sFAAsF;;AAQtF,0CASC;AATD,SAAgB,eAAe,CAAC,GAAsB;IAClD,MAAM,GAAG,GACJ,GAAG,CAAC,MAAc,EAAE,SAAS;QAC7B,GAAG,CAAC,KAAa,EAAE,SAAS;QAC5B,GAAG,CAAC,OAAe,EAAE,CAAC,aAAa,CAAC,CAAC;IAE1C,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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mybuildex/pkg",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Infra comum das APIs MyBuilder: multitenancy Prisma (pool único), middleware de tenant e CORS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/my-build-ead/mybuilder-typescript-pkg.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"fastify",
|
|
18
|
+
"prisma",
|
|
19
|
+
"multitenant",
|
|
20
|
+
"mariadb"
|
|
21
|
+
],
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"types": "dist/index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"prepack": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@fastify/cors": "^8.4.1",
|
|
35
|
+
"@fastify/swagger": "^8.12.0",
|
|
36
|
+
"@fastify/swagger-ui": "^1.10.1",
|
|
37
|
+
"@google/genai": "^2.13.0",
|
|
38
|
+
"@prisma/adapter-mariadb": "7.10.0",
|
|
39
|
+
"@prisma/client": "7.10.0",
|
|
40
|
+
"firebase": "^10.6.0",
|
|
41
|
+
"firebase-admin": "^11.11.0",
|
|
42
|
+
"mariadb": "3.4.5",
|
|
43
|
+
"prisma": "7.10.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"fastify": ">=4"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"fastify": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.0.0",
|
|
55
|
+
"fastify": "^4.26.2",
|
|
56
|
+
"typescript": "^5.5.0",
|
|
57
|
+
"vitest": "^3.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|