@usebetterdev/tenant-core 0.1.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/adapter.d.ts +24 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/api.d.ts +33 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/better-tenant.d.ts +40 -0
- package/dist/better-tenant.d.ts.map +1 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/handle-request.d.ts +17 -0
- package/dist/handle-request.d.ts.map +1 -0
- package/dist/index.cjs +677 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +636 -0
- package/dist/index.js.map +1 -0
- package/dist/request.d.ts +12 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/resolver.d.ts +11 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/telemetry.d.ts +97 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
// src/telemetry.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { platform, release, arch, cpus, totalmem } from "os";
|
|
6
|
+
var TELEMETRY_ENDPOINT = "https://telemetry.usebetter.dev/v1/events";
|
|
7
|
+
var LIBRARY_ID = "better-tenant";
|
|
8
|
+
function getTelemetryTenantConfig(config) {
|
|
9
|
+
const r = config.tenantResolver ?? {};
|
|
10
|
+
return {
|
|
11
|
+
resolver: {
|
|
12
|
+
header: !!r.header,
|
|
13
|
+
path: !!r.path,
|
|
14
|
+
subdomain: !!r.subdomain,
|
|
15
|
+
jwt: !!r.jwt,
|
|
16
|
+
custom: !!r.custom
|
|
17
|
+
},
|
|
18
|
+
tenantTablesCount: config.tenantTables?.length ?? 0,
|
|
19
|
+
hasGetTenantRepository: !!config.getTenantRepository,
|
|
20
|
+
loadTenant: config.loadTenant,
|
|
21
|
+
basePathSet: !!config.basePath,
|
|
22
|
+
plugins: (config.plugins ?? []).map((p) => String(p.id))
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function getTelemetryCliConfig(config) {
|
|
26
|
+
return {
|
|
27
|
+
tenantTablesCount: config.tenantTables?.length ?? 0,
|
|
28
|
+
hasSchemaDir: !!config.schemaDir,
|
|
29
|
+
hasMigrationsDir: !!config.migrationsDir
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function getAnonymousId(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const pkgPath = join(cwd, "package.json");
|
|
35
|
+
if (!existsSync(pkgPath)) return void 0;
|
|
36
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
const name = typeof pkg?.name === "string" ? pkg.name : "";
|
|
38
|
+
const basePath = typeof pkg?.betterTenant?.basePath === "string" ? pkg.betterTenant.basePath : "";
|
|
39
|
+
const input = `${name}:${basePath}`.trim() || "unknown";
|
|
40
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function detectRuntime() {
|
|
46
|
+
const g = globalThis;
|
|
47
|
+
if (typeof g.Bun !== "undefined") {
|
|
48
|
+
const bun = g.Bun;
|
|
49
|
+
return {
|
|
50
|
+
name: "bun",
|
|
51
|
+
version: typeof bun?.version === "string" ? bun.version : "unknown"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (typeof g.Deno !== "undefined") {
|
|
55
|
+
const deno = g.Deno;
|
|
56
|
+
const v2 = deno?.version?.deno;
|
|
57
|
+
return { name: "deno", version: typeof v2 === "string" ? v2 : "unknown" };
|
|
58
|
+
}
|
|
59
|
+
const v = typeof process !== "undefined" && process.versions?.node || "";
|
|
60
|
+
return { name: "node", version: v || "unknown" };
|
|
61
|
+
}
|
|
62
|
+
function detectEnvironment() {
|
|
63
|
+
const env = process.env.NODE_ENV || "development";
|
|
64
|
+
if (env === "test") return "test";
|
|
65
|
+
if (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.GITLAB_CI === "true" || process.env.CIRCLECI === "true") {
|
|
66
|
+
return "ci";
|
|
67
|
+
}
|
|
68
|
+
if (env === "production") return "production";
|
|
69
|
+
return "development";
|
|
70
|
+
}
|
|
71
|
+
function detectFramework(cwd) {
|
|
72
|
+
try {
|
|
73
|
+
const pkgPath = join(cwd, "package.json");
|
|
74
|
+
if (!existsSync(pkgPath)) return void 0;
|
|
75
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
76
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
77
|
+
if (deps["next"]) return { name: "next", version: deps["next"] };
|
|
78
|
+
if (deps["hono"]) return { name: "hono", version: deps["hono"] };
|
|
79
|
+
if (deps["express"]) return { name: "express", version: deps["express"] };
|
|
80
|
+
return void 0;
|
|
81
|
+
} catch {
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function detectDatabase(cwd) {
|
|
86
|
+
try {
|
|
87
|
+
const pkgPath = join(cwd, "package.json");
|
|
88
|
+
if (!existsSync(pkgPath)) return void 0;
|
|
89
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
90
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
91
|
+
if (deps["drizzle-orm"])
|
|
92
|
+
return { name: "drizzle", version: deps["drizzle-orm"] };
|
|
93
|
+
if (deps["prisma"]) return { name: "prisma", version: deps["prisma"] };
|
|
94
|
+
return void 0;
|
|
95
|
+
} catch {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function detectSystem() {
|
|
100
|
+
const isDocker = existsSync("/.dockerenv");
|
|
101
|
+
const isWSL = typeof process.env.WSL_DISTRO_NAME === "string" || typeof process.env.WSL_INTEROP === "string" && process.env.WSL_INTEROP.length > 0;
|
|
102
|
+
return {
|
|
103
|
+
platform: platform(),
|
|
104
|
+
release: release(),
|
|
105
|
+
arch: arch(),
|
|
106
|
+
cpus: cpus().length,
|
|
107
|
+
totalMemoryMb: Math.round(totalmem() / 1024 / 1024),
|
|
108
|
+
isDocker,
|
|
109
|
+
isWSL
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function detectPackageManager() {
|
|
113
|
+
const ua = process.env.npm_config_user_agent;
|
|
114
|
+
if (!ua || typeof ua !== "string") return void 0;
|
|
115
|
+
const match = ua.match(/^(.+?)\/(\d+\.\d+\.\d+.*?)(?:\s|$)/);
|
|
116
|
+
if (match) {
|
|
117
|
+
const name = (match[1] ?? "unknown").toLowerCase();
|
|
118
|
+
const version = match[2];
|
|
119
|
+
return version ? { name, version } : { name };
|
|
120
|
+
}
|
|
121
|
+
if (ua.includes("pnpm")) return { name: "pnpm" };
|
|
122
|
+
if (ua.includes("yarn")) return { name: "yarn" };
|
|
123
|
+
return { name: "npm" };
|
|
124
|
+
}
|
|
125
|
+
function isTelemetryEnabled(options) {
|
|
126
|
+
if (process.env.NODE_ENV === "test") return false;
|
|
127
|
+
const env = process.env.BETTER_TENANT_TELEMETRY;
|
|
128
|
+
if (env === "0" || env?.toLowerCase() === "false") return false;
|
|
129
|
+
if (env === "1" || env?.toLowerCase() === "true") return true;
|
|
130
|
+
return options?.enabled !== false;
|
|
131
|
+
}
|
|
132
|
+
function isDebugMode(options) {
|
|
133
|
+
if (process.env.BETTER_TENANT_TELEMETRY_DEBUG === "1") return true;
|
|
134
|
+
return options?.debug === true;
|
|
135
|
+
}
|
|
136
|
+
function sendTelemetry(type, payload, options) {
|
|
137
|
+
if (!isTelemetryEnabled(options))
|
|
138
|
+
return options?.wait ? Promise.resolve() : void 0;
|
|
139
|
+
const fullPayload = {
|
|
140
|
+
library: LIBRARY_ID,
|
|
141
|
+
type,
|
|
142
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
143
|
+
...payload
|
|
144
|
+
};
|
|
145
|
+
if (isDebugMode(options)) {
|
|
146
|
+
console.log(
|
|
147
|
+
"[better-tenant telemetry]",
|
|
148
|
+
JSON.stringify(fullPayload, null, 2)
|
|
149
|
+
);
|
|
150
|
+
return options?.wait ? Promise.resolve() : void 0;
|
|
151
|
+
}
|
|
152
|
+
const doSend = () => fetch(TELEMETRY_ENDPOINT, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "Content-Type": "application/json" },
|
|
155
|
+
body: JSON.stringify(fullPayload)
|
|
156
|
+
}).catch(() => {
|
|
157
|
+
});
|
|
158
|
+
if (options?.wait) {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
setImmediate(async () => {
|
|
161
|
+
await doSend();
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
setImmediate(() => doSend());
|
|
167
|
+
}
|
|
168
|
+
function sendInitTelemetry(config, options) {
|
|
169
|
+
const cwd = process.cwd();
|
|
170
|
+
const payload = {
|
|
171
|
+
runtime: detectRuntime(),
|
|
172
|
+
environment: detectEnvironment(),
|
|
173
|
+
config: getTelemetryTenantConfig(config)
|
|
174
|
+
};
|
|
175
|
+
const anonymousId = getAnonymousId(cwd);
|
|
176
|
+
if (anonymousId) payload.anonymousId = anonymousId;
|
|
177
|
+
const framework = detectFramework(cwd);
|
|
178
|
+
if (framework) payload.framework = framework;
|
|
179
|
+
const database = detectDatabase(cwd);
|
|
180
|
+
if (database) payload.database = database;
|
|
181
|
+
payload.system = detectSystem();
|
|
182
|
+
const pkgMgr = detectPackageManager();
|
|
183
|
+
if (pkgMgr) payload.packageManager = pkgMgr;
|
|
184
|
+
sendTelemetry("init", payload, options ?? config.telemetry);
|
|
185
|
+
}
|
|
186
|
+
function sendCliTelemetry(type, outcome, config, options) {
|
|
187
|
+
const cwd = process.cwd();
|
|
188
|
+
const redactedConfig = config ? getTelemetryCliConfig(config) : void 0;
|
|
189
|
+
const payload = {
|
|
190
|
+
runtime: detectRuntime(),
|
|
191
|
+
environment: detectEnvironment(),
|
|
192
|
+
outcome
|
|
193
|
+
};
|
|
194
|
+
if (redactedConfig) payload.config = redactedConfig;
|
|
195
|
+
payload.system = detectSystem();
|
|
196
|
+
const anonymousId = getAnonymousId(cwd);
|
|
197
|
+
if (anonymousId) payload.anonymousId = anonymousId;
|
|
198
|
+
const framework = detectFramework(cwd);
|
|
199
|
+
if (framework) payload.framework = framework;
|
|
200
|
+
const database = detectDatabase(cwd);
|
|
201
|
+
if (database) payload.database = database;
|
|
202
|
+
const pkgMgr = detectPackageManager();
|
|
203
|
+
if (pkgMgr) payload.packageManager = pkgMgr;
|
|
204
|
+
return sendTelemetry(type, payload, options);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/context.ts
|
|
208
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
209
|
+
var storage = new AsyncLocalStorage();
|
|
210
|
+
function getContext() {
|
|
211
|
+
return storage.getStore();
|
|
212
|
+
}
|
|
213
|
+
function runWithTenant(tenantId, fn) {
|
|
214
|
+
const normalized = tenantId.trim() || void 0;
|
|
215
|
+
if (normalized === void 0) {
|
|
216
|
+
return Promise.reject(
|
|
217
|
+
new Error("better-tenant: tenantId must be a non-empty string")
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return Promise.resolve(
|
|
221
|
+
storage.run({ tenantId: normalized }, () => fn())
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
function runWithContext(context, fn) {
|
|
225
|
+
return Promise.resolve(storage.run(context, () => fn()));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/adapter.ts
|
|
229
|
+
function getDatabase() {
|
|
230
|
+
const context = getContext();
|
|
231
|
+
return context?.database;
|
|
232
|
+
}
|
|
233
|
+
async function runWithTenantAndDatabase(tenantId, adapter, fn, options) {
|
|
234
|
+
const result = await adapter.runWithTenant(tenantId, async (database) => {
|
|
235
|
+
let context = { tenantId, database };
|
|
236
|
+
if (options?.loadTenant && options.getTenantRepository && adapter.runAsSystem) {
|
|
237
|
+
const tenant = await adapter.runAsSystem(
|
|
238
|
+
(systemDb) => options.getTenantRepository(systemDb).getById(tenantId)
|
|
239
|
+
);
|
|
240
|
+
context = tenant != null ? { ...context, tenant } : context;
|
|
241
|
+
}
|
|
242
|
+
return runWithContext(context, () => fn(database));
|
|
243
|
+
});
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/errors.ts
|
|
248
|
+
var TenantNotResolvedError = class extends Error {
|
|
249
|
+
constructor(message = "better-tenant: tenant could not be resolved from request") {
|
|
250
|
+
super(message);
|
|
251
|
+
this.name = "TenantNotResolvedError";
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var TenantMiddlewareError = class extends Error {
|
|
255
|
+
constructor(message = "better-tenant: failed to run tenant middleware", options) {
|
|
256
|
+
super(message, options);
|
|
257
|
+
this.name = "TenantMiddlewareError";
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/handle-request.ts
|
|
262
|
+
async function handleRequest(request, next, options) {
|
|
263
|
+
const tenantId = await options.resolveTenant(request);
|
|
264
|
+
const normalizedTenantId = tenantId?.trim();
|
|
265
|
+
if (!normalizedTenantId) {
|
|
266
|
+
if (options.onMissingTenant) {
|
|
267
|
+
return options.onMissingTenant(request);
|
|
268
|
+
}
|
|
269
|
+
const error = new TenantNotResolvedError();
|
|
270
|
+
if (options.onError) {
|
|
271
|
+
return options.onError(error, request);
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
return await runWithTenantAndDatabase(
|
|
277
|
+
normalizedTenantId,
|
|
278
|
+
options.adapter,
|
|
279
|
+
async () => next()
|
|
280
|
+
);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (options.onError) {
|
|
283
|
+
return options.onError(error, request);
|
|
284
|
+
}
|
|
285
|
+
if (error instanceof TenantNotResolvedError || error instanceof TenantMiddlewareError) {
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
throw new TenantMiddlewareError(void 0, { cause: error });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/api.ts
|
|
293
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
294
|
+
var MAX_LIST_LIMIT = 50;
|
|
295
|
+
function requireRunAsSystem(adapter) {
|
|
296
|
+
if (!adapter.runAsSystem) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
"better-tenant: tenant.api and runAsSystem require adapter.runAsSystem; this adapter does not implement it"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return adapter.runAsSystem;
|
|
302
|
+
}
|
|
303
|
+
function requireTenantRepository(getTenantRepository) {
|
|
304
|
+
if (!getTenantRepository) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
"better-tenant: tenant.api requires getTenantRepository in config (adapter provides CRUD for tenants table)"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return getTenantRepository;
|
|
310
|
+
}
|
|
311
|
+
function createTenantApi(adapter, getTenantRepository) {
|
|
312
|
+
const runAsSystem2 = requireRunAsSystem(adapter);
|
|
313
|
+
const getRepository = requireTenantRepository(getTenantRepository);
|
|
314
|
+
return {
|
|
315
|
+
async createTenant(data) {
|
|
316
|
+
if (!data.name?.trim()) {
|
|
317
|
+
throw new Error("better-tenant: createTenant requires name");
|
|
318
|
+
}
|
|
319
|
+
if (!data.slug?.trim()) {
|
|
320
|
+
throw new Error("better-tenant: createTenant requires slug");
|
|
321
|
+
}
|
|
322
|
+
return runAsSystem2(
|
|
323
|
+
(database) => getRepository(database).create({
|
|
324
|
+
name: data.name.trim(),
|
|
325
|
+
slug: data.slug.trim()
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
async updateTenant(tenantId, data) {
|
|
330
|
+
if (!tenantId?.trim()) {
|
|
331
|
+
throw new Error("better-tenant: updateTenant requires tenantId");
|
|
332
|
+
}
|
|
333
|
+
return runAsSystem2(
|
|
334
|
+
(database) => getRepository(database).update(tenantId, {
|
|
335
|
+
...data.name !== void 0 && { name: data.name },
|
|
336
|
+
...data.slug !== void 0 && { slug: data.slug }
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
},
|
|
340
|
+
async listTenants(options = {}) {
|
|
341
|
+
const limit = Math.min(
|
|
342
|
+
options.limit ?? DEFAULT_LIST_LIMIT,
|
|
343
|
+
MAX_LIST_LIMIT
|
|
344
|
+
);
|
|
345
|
+
const offset = Math.max(0, options.offset ?? 0);
|
|
346
|
+
return runAsSystem2(
|
|
347
|
+
(database) => getRepository(database).list({ limit, offset })
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
async deleteTenant(tenantId) {
|
|
351
|
+
if (!tenantId?.trim()) {
|
|
352
|
+
throw new Error("better-tenant: deleteTenant requires tenantId");
|
|
353
|
+
}
|
|
354
|
+
return runAsSystem2(
|
|
355
|
+
(database) => getRepository(database).delete(tenantId)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
async function runAs(tenantId, adapter, fn) {
|
|
361
|
+
return runWithTenantAndDatabase(tenantId, adapter, fn);
|
|
362
|
+
}
|
|
363
|
+
async function runAsSystem(adapter, fn) {
|
|
364
|
+
const run = requireRunAsSystem(adapter);
|
|
365
|
+
return runWithContext(
|
|
366
|
+
{ tenantId: "", isSystem: true },
|
|
367
|
+
() => run(fn)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/resolver.ts
|
|
372
|
+
function getHeader(headers, name) {
|
|
373
|
+
const key = name.toLowerCase();
|
|
374
|
+
if (headers instanceof Headers) {
|
|
375
|
+
const value2 = headers.get(key);
|
|
376
|
+
return value2?.trim() || void 0;
|
|
377
|
+
}
|
|
378
|
+
const raw = headers[key] ?? headers[name];
|
|
379
|
+
if (raw === void 0) return void 0;
|
|
380
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
381
|
+
return (typeof value === "string" ? value : "").trim() || void 0;
|
|
382
|
+
}
|
|
383
|
+
function resolveFromHeader(request, headerName) {
|
|
384
|
+
if (!headerName) return void 0;
|
|
385
|
+
return getHeader(request.headers, headerName);
|
|
386
|
+
}
|
|
387
|
+
function resolveFromPath(request, pathConfig) {
|
|
388
|
+
const pathOrUrl = request.path ?? request.url;
|
|
389
|
+
if (!pathOrUrl) return void 0;
|
|
390
|
+
let path;
|
|
391
|
+
try {
|
|
392
|
+
path = pathOrUrl.startsWith("http") ? new URL(pathOrUrl).pathname : pathOrUrl;
|
|
393
|
+
} catch {
|
|
394
|
+
path = pathOrUrl;
|
|
395
|
+
}
|
|
396
|
+
const pattern = typeof pathConfig === "string" ? pathConfig : pathConfig.pattern;
|
|
397
|
+
const segmentIndex = typeof pathConfig === "object" && pathConfig.segmentIndex !== void 0 ? pathConfig.segmentIndex : parseTenantSegmentIndex(pattern);
|
|
398
|
+
const segments = path.split("/").filter(Boolean);
|
|
399
|
+
const value = segments[segmentIndex];
|
|
400
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
401
|
+
}
|
|
402
|
+
function parseTenantSegmentIndex(pattern) {
|
|
403
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
404
|
+
const idx = segments.indexOf(":tenantId");
|
|
405
|
+
return idx >= 0 ? idx : 0;
|
|
406
|
+
}
|
|
407
|
+
function resolveFromSubdomain(request, config) {
|
|
408
|
+
const host = request.host ?? (request.url ? new URL(request.url).host : "");
|
|
409
|
+
if (!host) return void 0;
|
|
410
|
+
const hostname = host.split(":")[0];
|
|
411
|
+
if (!hostname) return void 0;
|
|
412
|
+
const parts = hostname.split(".");
|
|
413
|
+
if (parts.length <= 2) return void 0;
|
|
414
|
+
const index = config === true ? 0 : typeof config === "object" && config.segmentIndex !== void 0 ? config.segmentIndex : 0;
|
|
415
|
+
const value = parts[index];
|
|
416
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
417
|
+
}
|
|
418
|
+
function decodeJwtPayload(token) {
|
|
419
|
+
try {
|
|
420
|
+
const parts = token.split(".");
|
|
421
|
+
if (parts.length < 2) return null;
|
|
422
|
+
const payload = parts[1];
|
|
423
|
+
if (!payload) return null;
|
|
424
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
425
|
+
return JSON.parse(decoded);
|
|
426
|
+
} catch {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function resolveFromJwt(request, config) {
|
|
431
|
+
const getToken = request.getToken;
|
|
432
|
+
if (!getToken) return void 0;
|
|
433
|
+
const token = typeof getToken === "function" ? getToken() : getToken;
|
|
434
|
+
const value = token instanceof Promise ? void 0 : token ?? void 0;
|
|
435
|
+
const resolved = value ?? void 0;
|
|
436
|
+
if (!resolved) return void 0;
|
|
437
|
+
const claim = typeof config === "string" ? config : config.claim;
|
|
438
|
+
if (!claim) return void 0;
|
|
439
|
+
const payload = decodeJwtPayload(resolved);
|
|
440
|
+
if (!payload) return void 0;
|
|
441
|
+
const claimValue = payload[claim];
|
|
442
|
+
return typeof claimValue === "string" && claimValue.length > 0 ? claimValue : void 0;
|
|
443
|
+
}
|
|
444
|
+
async function resolveFromJwtAsync(request, config) {
|
|
445
|
+
const getToken = request.getToken;
|
|
446
|
+
if (!getToken) return void 0;
|
|
447
|
+
const token = typeof getToken === "function" ? getToken() : getToken;
|
|
448
|
+
const value = token instanceof Promise ? await token : token;
|
|
449
|
+
const resolved = value ?? void 0;
|
|
450
|
+
if (!resolved) return void 0;
|
|
451
|
+
const claim = typeof config === "string" ? config : config.claim;
|
|
452
|
+
if (!claim) return void 0;
|
|
453
|
+
const payload = decodeJwtPayload(resolved);
|
|
454
|
+
if (!payload) return void 0;
|
|
455
|
+
const claimValue = payload[claim];
|
|
456
|
+
return typeof claimValue === "string" && claimValue.length > 0 ? claimValue : void 0;
|
|
457
|
+
}
|
|
458
|
+
function resolveTenant(request, config) {
|
|
459
|
+
if (config.header !== void 0) {
|
|
460
|
+
const v = resolveFromHeader(request, config.header);
|
|
461
|
+
if (v !== void 0) return v;
|
|
462
|
+
}
|
|
463
|
+
if (config.path !== void 0) {
|
|
464
|
+
const v = resolveFromPath(request, config.path);
|
|
465
|
+
if (v !== void 0) return v;
|
|
466
|
+
}
|
|
467
|
+
if (config.subdomain !== void 0 && config.subdomain !== false) {
|
|
468
|
+
const v = resolveFromSubdomain(request, config.subdomain);
|
|
469
|
+
if (v !== void 0) return v;
|
|
470
|
+
}
|
|
471
|
+
if (config.jwt !== void 0) {
|
|
472
|
+
const v = resolveFromJwt(request, config.jwt);
|
|
473
|
+
if (v !== void 0) return v;
|
|
474
|
+
}
|
|
475
|
+
if (config.custom !== void 0) {
|
|
476
|
+
const v = config.custom(request);
|
|
477
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
478
|
+
}
|
|
479
|
+
return void 0;
|
|
480
|
+
}
|
|
481
|
+
async function resolveTenantAsync(request, config) {
|
|
482
|
+
if (config.header !== void 0) {
|
|
483
|
+
const v = resolveFromHeader(request, config.header);
|
|
484
|
+
if (v !== void 0) return v;
|
|
485
|
+
}
|
|
486
|
+
if (config.path !== void 0) {
|
|
487
|
+
const v = resolveFromPath(request, config.path);
|
|
488
|
+
if (v !== void 0) return v;
|
|
489
|
+
}
|
|
490
|
+
if (config.subdomain !== void 0 && config.subdomain !== false) {
|
|
491
|
+
const v = resolveFromSubdomain(request, config.subdomain);
|
|
492
|
+
if (v !== void 0) return v;
|
|
493
|
+
}
|
|
494
|
+
if (config.jwt !== void 0) {
|
|
495
|
+
const v = await resolveFromJwtAsync(request, config.jwt);
|
|
496
|
+
if (v !== void 0) return v;
|
|
497
|
+
}
|
|
498
|
+
if (config.custom !== void 0) {
|
|
499
|
+
const v = await Promise.resolve(config.custom(request));
|
|
500
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
501
|
+
}
|
|
502
|
+
return void 0;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/better-tenant.ts
|
|
506
|
+
function betterTenant(config) {
|
|
507
|
+
const { adapter, tenantResolver, getTenantRepository, loadTenant } = config;
|
|
508
|
+
sendInitTelemetry(config, config.telemetry);
|
|
509
|
+
const api = getTenantRepository ? createTenantApi(adapter, getTenantRepository) : createStubTenantApi();
|
|
510
|
+
const shouldLoadTenant = getTenantRepository != null && loadTenant !== false;
|
|
511
|
+
const runWithTenantAndDatabaseOptions = shouldLoadTenant ? { loadTenant: true, getTenantRepository } : void 0;
|
|
512
|
+
return {
|
|
513
|
+
getContext,
|
|
514
|
+
getDatabase,
|
|
515
|
+
runWithTenant,
|
|
516
|
+
runWithTenantAndDatabase: (tenantId, _adapter, fn) => runWithTenantAndDatabase(tenantId, adapter, fn, runWithTenantAndDatabaseOptions),
|
|
517
|
+
runAs: (tenantId, _adapter, fn) => runAs(tenantId, adapter, fn),
|
|
518
|
+
runAsSystem: (fn) => runAsSystem(adapter, fn),
|
|
519
|
+
resolveTenant: (request) => resolveTenant(request, tenantResolver),
|
|
520
|
+
resolveTenantAsync: (request) => resolveTenantAsync(request, tenantResolver),
|
|
521
|
+
handleRequest: (request, next, options) => handleRequest(request, next, {
|
|
522
|
+
...options,
|
|
523
|
+
resolveTenant: (input) => resolveTenantAsync(input, tenantResolver),
|
|
524
|
+
adapter
|
|
525
|
+
}),
|
|
526
|
+
tenant: { api }
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function createStubTenantApi() {
|
|
530
|
+
const err = () => {
|
|
531
|
+
throw new Error(
|
|
532
|
+
"better-tenant: tenant.api requires getTenantRepository in config"
|
|
533
|
+
);
|
|
534
|
+
};
|
|
535
|
+
return {
|
|
536
|
+
createTenant: () => err(),
|
|
537
|
+
updateTenant: () => err(),
|
|
538
|
+
listTenants: () => err(),
|
|
539
|
+
deleteTenant: () => err()
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/request.ts
|
|
544
|
+
function isFetchRequest(input) {
|
|
545
|
+
return typeof Request !== "undefined" && input instanceof Request;
|
|
546
|
+
}
|
|
547
|
+
function normalizeHost(raw) {
|
|
548
|
+
if (!raw) return void 0;
|
|
549
|
+
const value = raw.split(":")[0]?.trim();
|
|
550
|
+
return value || void 0;
|
|
551
|
+
}
|
|
552
|
+
function normalizeHeaders(headers) {
|
|
553
|
+
if (!headers) return {};
|
|
554
|
+
const normalized = {};
|
|
555
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
556
|
+
const normalizedKey = key.toLowerCase();
|
|
557
|
+
if (Array.isArray(value)) {
|
|
558
|
+
normalized[normalizedKey] = value.map((part) => String(part));
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (value === void 0) {
|
|
562
|
+
normalized[normalizedKey] = void 0;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
normalized[normalizedKey] = String(value);
|
|
566
|
+
}
|
|
567
|
+
return normalized;
|
|
568
|
+
}
|
|
569
|
+
function pathWithoutQuery(value) {
|
|
570
|
+
if (!value) return void 0;
|
|
571
|
+
const [pathname] = value.split("?");
|
|
572
|
+
return pathname || void 0;
|
|
573
|
+
}
|
|
574
|
+
function toResolvableRequest(request) {
|
|
575
|
+
if (isFetchRequest(request)) {
|
|
576
|
+
const url = request.url;
|
|
577
|
+
let host2;
|
|
578
|
+
let path2;
|
|
579
|
+
try {
|
|
580
|
+
const parsedUrl = new URL(url);
|
|
581
|
+
host2 = normalizeHost(parsedUrl.host);
|
|
582
|
+
path2 = parsedUrl.pathname || void 0;
|
|
583
|
+
} catch {
|
|
584
|
+
host2 = void 0;
|
|
585
|
+
path2 = void 0;
|
|
586
|
+
}
|
|
587
|
+
const resolved2 = {
|
|
588
|
+
headers: request.headers,
|
|
589
|
+
url,
|
|
590
|
+
method: request.method
|
|
591
|
+
};
|
|
592
|
+
if (path2 !== void 0) {
|
|
593
|
+
resolved2.path = path2;
|
|
594
|
+
}
|
|
595
|
+
if (host2 !== void 0) {
|
|
596
|
+
resolved2.host = host2;
|
|
597
|
+
}
|
|
598
|
+
return resolved2;
|
|
599
|
+
}
|
|
600
|
+
const headers = normalizeHeaders(request.headers);
|
|
601
|
+
const hostFromHeaders = headers.host;
|
|
602
|
+
const hostValue = Array.isArray(hostFromHeaders) ? hostFromHeaders[0] : hostFromHeaders;
|
|
603
|
+
const host = normalizeHost(request.hostname ?? request.host ?? hostValue);
|
|
604
|
+
const path = pathWithoutQuery(request.path ?? request.originalUrl ?? request.url);
|
|
605
|
+
const resolved = {
|
|
606
|
+
headers
|
|
607
|
+
};
|
|
608
|
+
if (path !== void 0) {
|
|
609
|
+
resolved.path = path;
|
|
610
|
+
}
|
|
611
|
+
if (host !== void 0) {
|
|
612
|
+
resolved.host = host;
|
|
613
|
+
}
|
|
614
|
+
if (request.method !== void 0) {
|
|
615
|
+
resolved.method = request.method;
|
|
616
|
+
}
|
|
617
|
+
return resolved;
|
|
618
|
+
}
|
|
619
|
+
export {
|
|
620
|
+
TenantMiddlewareError,
|
|
621
|
+
TenantNotResolvedError,
|
|
622
|
+
betterTenant,
|
|
623
|
+
createTenantApi,
|
|
624
|
+
getContext,
|
|
625
|
+
getDatabase,
|
|
626
|
+
handleRequest,
|
|
627
|
+
resolveTenant,
|
|
628
|
+
resolveTenantAsync,
|
|
629
|
+
runAs,
|
|
630
|
+
runAsSystem,
|
|
631
|
+
runWithTenant,
|
|
632
|
+
runWithTenantAndDatabase,
|
|
633
|
+
sendCliTelemetry,
|
|
634
|
+
toResolvableRequest
|
|
635
|
+
};
|
|
636
|
+
//# sourceMappingURL=index.js.map
|