@usebetterdev/tenant-core 0.1.0 → 0.2.0-beta.12

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/dist/index.js CHANGED
@@ -15,8 +15,7 @@ function getTelemetryTenantConfig(config) {
15
15
  jwt: !!r.jwt,
16
16
  custom: !!r.custom
17
17
  },
18
- tenantTablesCount: config.tenantTables?.length ?? 0,
19
- hasGetTenantRepository: !!config.getTenantRepository,
18
+ tenantTablesCount: 0,
20
19
  loadTenant: config.loadTenant,
21
20
  basePathSet: !!config.basePath,
22
21
  plugins: (config.plugins ?? []).map((p) => String(p.id))
@@ -32,7 +31,9 @@ function getTelemetryCliConfig(config) {
32
31
  function getAnonymousId(cwd) {
33
32
  try {
34
33
  const pkgPath = join(cwd, "package.json");
35
- if (!existsSync(pkgPath)) return void 0;
34
+ if (!existsSync(pkgPath)) {
35
+ return void 0;
36
+ }
36
37
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
37
38
  const name = typeof pkg?.name === "string" ? pkg.name : "";
38
39
  const basePath = typeof pkg?.betterTenant?.basePath === "string" ? pkg.betterTenant.basePath : "";
@@ -61,22 +62,34 @@ function detectRuntime() {
61
62
  }
62
63
  function detectEnvironment() {
63
64
  const env = process.env.NODE_ENV || "development";
64
- if (env === "test") return "test";
65
+ if (env === "test") {
66
+ return "test";
67
+ }
65
68
  if (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.GITLAB_CI === "true" || process.env.CIRCLECI === "true") {
66
69
  return "ci";
67
70
  }
68
- if (env === "production") return "production";
71
+ if (env === "production") {
72
+ return "production";
73
+ }
69
74
  return "development";
70
75
  }
71
76
  function detectFramework(cwd) {
72
77
  try {
73
78
  const pkgPath = join(cwd, "package.json");
74
- if (!existsSync(pkgPath)) return void 0;
79
+ if (!existsSync(pkgPath)) {
80
+ return void 0;
81
+ }
75
82
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
76
83
  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"] };
84
+ if (deps["next"]) {
85
+ return { name: "next", version: deps["next"] };
86
+ }
87
+ if (deps["hono"]) {
88
+ return { name: "hono", version: deps["hono"] };
89
+ }
90
+ if (deps["express"]) {
91
+ return { name: "express", version: deps["express"] };
92
+ }
80
93
  return void 0;
81
94
  } catch {
82
95
  return void 0;
@@ -85,12 +98,17 @@ function detectFramework(cwd) {
85
98
  function detectDatabase(cwd) {
86
99
  try {
87
100
  const pkgPath = join(cwd, "package.json");
88
- if (!existsSync(pkgPath)) return void 0;
101
+ if (!existsSync(pkgPath)) {
102
+ return void 0;
103
+ }
89
104
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
90
105
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
91
- if (deps["drizzle-orm"])
106
+ if (deps["drizzle-orm"]) {
92
107
  return { name: "drizzle", version: deps["drizzle-orm"] };
93
- if (deps["prisma"]) return { name: "prisma", version: deps["prisma"] };
108
+ }
109
+ if (deps["prisma"]) {
110
+ return { name: "prisma", version: deps["prisma"] };
111
+ }
94
112
  return void 0;
95
113
  } catch {
96
114
  return void 0;
@@ -111,31 +129,46 @@ function detectSystem() {
111
129
  }
112
130
  function detectPackageManager() {
113
131
  const ua = process.env.npm_config_user_agent;
114
- if (!ua || typeof ua !== "string") return void 0;
132
+ if (!ua || typeof ua !== "string") {
133
+ return void 0;
134
+ }
115
135
  const match = ua.match(/^(.+?)\/(\d+\.\d+\.\d+.*?)(?:\s|$)/);
116
136
  if (match) {
117
137
  const name = (match[1] ?? "unknown").toLowerCase();
118
138
  const version = match[2];
119
139
  return version ? { name, version } : { name };
120
140
  }
121
- if (ua.includes("pnpm")) return { name: "pnpm" };
122
- if (ua.includes("yarn")) return { name: "yarn" };
141
+ if (ua.includes("pnpm")) {
142
+ return { name: "pnpm" };
143
+ }
144
+ if (ua.includes("yarn")) {
145
+ return { name: "yarn" };
146
+ }
123
147
  return { name: "npm" };
124
148
  }
125
149
  function isTelemetryEnabled(options) {
126
- if (process.env.NODE_ENV === "test") return false;
150
+ if (process.env.NODE_ENV === "test") {
151
+ return false;
152
+ }
127
153
  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;
154
+ if (env === "0" || env?.toLowerCase() === "false") {
155
+ return false;
156
+ }
157
+ if (env === "1" || env?.toLowerCase() === "true") {
158
+ return true;
159
+ }
130
160
  return options?.enabled !== false;
131
161
  }
132
162
  function isDebugMode(options) {
133
- if (process.env.BETTER_TENANT_TELEMETRY_DEBUG === "1") return true;
163
+ if (process.env.BETTER_TENANT_TELEMETRY_DEBUG === "1") {
164
+ return true;
165
+ }
134
166
  return options?.debug === true;
135
167
  }
136
168
  function sendTelemetry(type, payload, options) {
137
- if (!isTelemetryEnabled(options))
169
+ if (!isTelemetryEnabled(options)) {
138
170
  return options?.wait ? Promise.resolve() : void 0;
171
+ }
139
172
  const fullPayload = {
140
173
  library: LIBRARY_ID,
141
174
  type,
@@ -233,9 +266,10 @@ function getDatabase() {
233
266
  async function runWithTenantAndDatabase(tenantId, adapter, fn, options) {
234
267
  const result = await adapter.runWithTenant(tenantId, async (database) => {
235
268
  let context = { tenantId, database };
236
- if (options?.loadTenant && options.getTenantRepository && adapter.runAsSystem) {
269
+ const getTenantRepository = options?.getTenantRepository;
270
+ if (options?.loadTenant && getTenantRepository && adapter.runAsSystem) {
237
271
  const tenant = await adapter.runAsSystem(
238
- (systemDb) => options.getTenantRepository(systemDb).getById(tenantId)
272
+ (systemDb) => getTenantRepository(systemDb).getById(tenantId)
239
273
  );
240
274
  context = tenant != null ? { ...context, tenant } : context;
241
275
  }
@@ -300,17 +334,7 @@ function requireRunAsSystem(adapter) {
300
334
  }
301
335
  return adapter.runAsSystem;
302
336
  }
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
337
  function createTenantApi(adapter, getTenantRepository) {
312
- const runAsSystem2 = requireRunAsSystem(adapter);
313
- const getRepository = requireTenantRepository(getTenantRepository);
314
338
  return {
315
339
  async createTenant(data) {
316
340
  if (!data.name?.trim()) {
@@ -319,8 +343,9 @@ function createTenantApi(adapter, getTenantRepository) {
319
343
  if (!data.slug?.trim()) {
320
344
  throw new Error("better-tenant: createTenant requires slug");
321
345
  }
346
+ const runAsSystem2 = requireRunAsSystem(adapter);
322
347
  return runAsSystem2(
323
- (database) => getRepository(database).create({
348
+ (database) => getTenantRepository(database).create({
324
349
  name: data.name.trim(),
325
350
  slug: data.slug.trim()
326
351
  })
@@ -330,8 +355,9 @@ function createTenantApi(adapter, getTenantRepository) {
330
355
  if (!tenantId?.trim()) {
331
356
  throw new Error("better-tenant: updateTenant requires tenantId");
332
357
  }
358
+ const runAsSystem2 = requireRunAsSystem(adapter);
333
359
  return runAsSystem2(
334
- (database) => getRepository(database).update(tenantId, {
360
+ (database) => getTenantRepository(database).update(tenantId, {
335
361
  ...data.name !== void 0 && { name: data.name },
336
362
  ...data.slug !== void 0 && { slug: data.slug }
337
363
  })
@@ -343,16 +369,18 @@ function createTenantApi(adapter, getTenantRepository) {
343
369
  MAX_LIST_LIMIT
344
370
  );
345
371
  const offset = Math.max(0, options.offset ?? 0);
372
+ const runAsSystem2 = requireRunAsSystem(adapter);
346
373
  return runAsSystem2(
347
- (database) => getRepository(database).list({ limit, offset })
374
+ (database) => getTenantRepository(database).list({ limit, offset })
348
375
  );
349
376
  },
350
377
  async deleteTenant(tenantId) {
351
378
  if (!tenantId?.trim()) {
352
379
  throw new Error("better-tenant: deleteTenant requires tenantId");
353
380
  }
381
+ const runAsSystem2 = requireRunAsSystem(adapter);
354
382
  return runAsSystem2(
355
- (database) => getRepository(database).delete(tenantId)
383
+ (database) => getTenantRepository(database).delete(tenantId)
356
384
  );
357
385
  }
358
386
  };
@@ -362,10 +390,7 @@ async function runAs(tenantId, adapter, fn) {
362
390
  }
363
391
  async function runAsSystem(adapter, fn) {
364
392
  const run = requireRunAsSystem(adapter);
365
- return runWithContext(
366
- { tenantId: "", isSystem: true },
367
- () => run(fn)
368
- );
393
+ return runWithContext({ isSystem: true }, () => run(fn));
369
394
  }
370
395
 
371
396
  // src/resolver.ts
@@ -376,17 +401,23 @@ function getHeader(headers, name) {
376
401
  return value2?.trim() || void 0;
377
402
  }
378
403
  const raw = headers[key] ?? headers[name];
379
- if (raw === void 0) return void 0;
404
+ if (raw === void 0) {
405
+ return void 0;
406
+ }
380
407
  const value = Array.isArray(raw) ? raw[0] : raw;
381
408
  return (typeof value === "string" ? value : "").trim() || void 0;
382
409
  }
383
410
  function resolveFromHeader(request, headerName) {
384
- if (!headerName) return void 0;
411
+ if (!headerName) {
412
+ return void 0;
413
+ }
385
414
  return getHeader(request.headers, headerName);
386
415
  }
387
416
  function resolveFromPath(request, pathConfig) {
388
417
  const pathOrUrl = request.path ?? request.url;
389
- if (!pathOrUrl) return void 0;
418
+ if (!pathOrUrl) {
419
+ return void 0;
420
+ }
390
421
  let path;
391
422
  try {
392
423
  path = pathOrUrl.startsWith("http") ? new URL(pathOrUrl).pathname : pathOrUrl;
@@ -406,11 +437,17 @@ function parseTenantSegmentIndex(pattern) {
406
437
  }
407
438
  function resolveFromSubdomain(request, config) {
408
439
  const host = request.host ?? (request.url ? new URL(request.url).host : "");
409
- if (!host) return void 0;
440
+ if (!host) {
441
+ return void 0;
442
+ }
410
443
  const hostname = host.split(":")[0];
411
- if (!hostname) return void 0;
444
+ if (!hostname) {
445
+ return void 0;
446
+ }
412
447
  const parts = hostname.split(".");
413
- if (parts.length <= 2) return void 0;
448
+ if (parts.length <= 2) {
449
+ return void 0;
450
+ }
414
451
  const index = config === true ? 0 : typeof config === "object" && config.segmentIndex !== void 0 ? config.segmentIndex : 0;
415
452
  const value = parts[index];
416
453
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -418,125 +455,159 @@ function resolveFromSubdomain(request, config) {
418
455
  function decodeJwtPayload(token) {
419
456
  try {
420
457
  const parts = token.split(".");
421
- if (parts.length < 2) return null;
458
+ if (parts.length < 2) {
459
+ return null;
460
+ }
422
461
  const payload = parts[1];
423
- if (!payload) return null;
462
+ if (!payload) {
463
+ return null;
464
+ }
424
465
  const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
425
- return JSON.parse(decoded);
466
+ const parsed = JSON.parse(decoded);
467
+ if (typeof parsed !== "object" || parsed === null) {
468
+ return null;
469
+ }
470
+ return parsed;
426
471
  } catch {
427
472
  return null;
428
473
  }
429
474
  }
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) {
475
+ async function resolveFromJwt(request, config) {
445
476
  const getToken = request.getToken;
446
- if (!getToken) return void 0;
477
+ if (!getToken) {
478
+ return void 0;
479
+ }
447
480
  const token = typeof getToken === "function" ? getToken() : getToken;
448
481
  const value = token instanceof Promise ? await token : token;
449
482
  const resolved = value ?? void 0;
450
- if (!resolved) return void 0;
483
+ if (!resolved) {
484
+ return void 0;
485
+ }
451
486
  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;
487
+ if (!claim) {
488
+ return void 0;
489
+ }
490
+ const verifyToken = typeof config === "object" ? config.verifyToken : void 0;
491
+ let payload;
492
+ if (verifyToken) {
493
+ payload = await verifyToken(resolved);
494
+ } else {
495
+ payload = decodeJwtPayload(resolved);
496
+ }
497
+ if (!payload) {
498
+ return void 0;
499
+ }
455
500
  const claimValue = payload[claim];
456
501
  return typeof claimValue === "string" && claimValue.length > 0 ? claimValue : void 0;
457
502
  }
458
- function resolveTenant(request, config) {
503
+ function describeStrategies(config) {
504
+ const strategies = [];
459
505
  if (config.header !== void 0) {
460
- const v = resolveFromHeader(request, config.header);
461
- if (v !== void 0) return v;
506
+ strategies.push(`header '${config.header}'`);
462
507
  }
463
508
  if (config.path !== void 0) {
464
- const v = resolveFromPath(request, config.path);
465
- if (v !== void 0) return v;
509
+ const pattern = typeof config.path === "string" ? config.path : config.path.pattern;
510
+ strategies.push(`path '${pattern}'`);
466
511
  }
467
512
  if (config.subdomain !== void 0 && config.subdomain !== false) {
468
- const v = resolveFromSubdomain(request, config.subdomain);
469
- if (v !== void 0) return v;
513
+ strategies.push("subdomain");
470
514
  }
471
515
  if (config.jwt !== void 0) {
472
- const v = resolveFromJwt(request, config.jwt);
473
- if (v !== void 0) return v;
516
+ const claim = typeof config.jwt === "string" ? config.jwt : config.jwt.claim;
517
+ strategies.push(`jwt claim '${claim}'`);
474
518
  }
475
519
  if (config.custom !== void 0) {
476
- const v = config.custom(request);
477
- if (typeof v === "string" && v.length > 0) return v;
520
+ strategies.push("custom resolver");
478
521
  }
479
- return void 0;
522
+ return strategies;
480
523
  }
481
- async function resolveTenantAsync(request, config) {
524
+ async function resolveTenant(request, config) {
482
525
  if (config.header !== void 0) {
483
526
  const v = resolveFromHeader(request, config.header);
484
- if (v !== void 0) return v;
527
+ if (v !== void 0) {
528
+ return v;
529
+ }
485
530
  }
486
531
  if (config.path !== void 0) {
487
532
  const v = resolveFromPath(request, config.path);
488
- if (v !== void 0) return v;
533
+ if (v !== void 0) {
534
+ return v;
535
+ }
489
536
  }
490
537
  if (config.subdomain !== void 0 && config.subdomain !== false) {
491
538
  const v = resolveFromSubdomain(request, config.subdomain);
492
- if (v !== void 0) return v;
539
+ if (v !== void 0) {
540
+ return v;
541
+ }
493
542
  }
494
543
  if (config.jwt !== void 0) {
495
- const v = await resolveFromJwtAsync(request, config.jwt);
496
- if (v !== void 0) return v;
544
+ const v = await resolveFromJwt(request, config.jwt);
545
+ if (v !== void 0) {
546
+ return v;
547
+ }
497
548
  }
498
549
  if (config.custom !== void 0) {
499
550
  const v = await Promise.resolve(config.custom(request));
500
- if (typeof v === "string" && v.length > 0) return v;
551
+ if (typeof v === "string" && v.length > 0) {
552
+ return v;
553
+ }
501
554
  }
502
555
  return void 0;
503
556
  }
504
557
 
505
558
  // src/better-tenant.ts
559
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
560
+ async function resolveIdentifierToId(identifier, resolverConfig, adapter, getTenantRepository) {
561
+ if (resolverConfig.resolveToId) {
562
+ return resolverConfig.resolveToId(identifier);
563
+ }
564
+ if (UUID_RE.test(identifier)) {
565
+ return identifier;
566
+ }
567
+ if (adapter.runAsSystem) {
568
+ const tenant = await adapter.runAsSystem(
569
+ (systemDb) => getTenantRepository(systemDb).getBySlug(identifier)
570
+ );
571
+ return tenant?.id;
572
+ }
573
+ return identifier;
574
+ }
506
575
  function betterTenant(config) {
507
- const { adapter, tenantResolver, getTenantRepository, loadTenant } = config;
576
+ const { database, tenantResolver, loadTenant } = config;
577
+ const { adapter, getTenantRepository } = database;
508
578
  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;
579
+ const api = createTenantApi(adapter, getTenantRepository);
580
+ const runWithTenantAndDatabaseOptions = loadTenant !== false ? { loadTenant: true, getTenantRepository } : void 0;
581
+ const resolverStrategies = describeStrategies(tenantResolver);
582
+ async function resolveAndNormalize(request) {
583
+ const raw = await resolveTenant(request, tenantResolver);
584
+ if (!raw) return void 0;
585
+ return resolveIdentifierToId(
586
+ raw,
587
+ tenantResolver,
588
+ adapter,
589
+ getTenantRepository
590
+ );
591
+ }
512
592
  return {
513
593
  getContext,
514
- getDatabase,
594
+ getDatabase: () => getDatabase(),
515
595
  runWithTenant,
516
- runWithTenantAndDatabase: (tenantId, _adapter, fn) => runWithTenantAndDatabase(tenantId, adapter, fn, runWithTenantAndDatabaseOptions),
517
- runAs: (tenantId, _adapter, fn) => runAs(tenantId, adapter, fn),
596
+ runAs: (tenantId, fn) => runWithTenantAndDatabase(
597
+ tenantId,
598
+ adapter,
599
+ fn,
600
+ runWithTenantAndDatabaseOptions
601
+ ),
518
602
  runAsSystem: (fn) => runAsSystem(adapter, fn),
519
- resolveTenant: (request) => resolveTenant(request, tenantResolver),
520
- resolveTenantAsync: (request) => resolveTenantAsync(request, tenantResolver),
603
+ resolveTenant: resolveAndNormalize,
604
+ resolverStrategies,
521
605
  handleRequest: (request, next, options) => handleRequest(request, next, {
522
606
  ...options,
523
- resolveTenant: (input) => resolveTenantAsync(input, tenantResolver),
607
+ resolveTenant: resolveAndNormalize,
524
608
  adapter
525
609
  }),
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()
610
+ api
540
611
  };
541
612
  }
542
613
 
@@ -545,12 +616,16 @@ function isFetchRequest(input) {
545
616
  return typeof Request !== "undefined" && input instanceof Request;
546
617
  }
547
618
  function normalizeHost(raw) {
548
- if (!raw) return void 0;
619
+ if (!raw) {
620
+ return void 0;
621
+ }
549
622
  const value = raw.split(":")[0]?.trim();
550
623
  return value || void 0;
551
624
  }
552
625
  function normalizeHeaders(headers) {
553
- if (!headers) return {};
626
+ if (!headers) {
627
+ return {};
628
+ }
554
629
  const normalized = {};
555
630
  for (const [key, value] of Object.entries(headers)) {
556
631
  const normalizedKey = key.toLowerCase();
@@ -567,7 +642,9 @@ function normalizeHeaders(headers) {
567
642
  return normalized;
568
643
  }
569
644
  function pathWithoutQuery(value) {
570
- if (!value) return void 0;
645
+ if (!value) {
646
+ return void 0;
647
+ }
571
648
  const [pathname] = value.split("?");
572
649
  return pathname || void 0;
573
650
  }
@@ -601,7 +678,9 @@ function toResolvableRequest(request) {
601
678
  const hostFromHeaders = headers.host;
602
679
  const hostValue = Array.isArray(hostFromHeaders) ? hostFromHeaders[0] : hostFromHeaders;
603
680
  const host = normalizeHost(request.hostname ?? request.host ?? hostValue);
604
- const path = pathWithoutQuery(request.path ?? request.originalUrl ?? request.url);
681
+ const path = pathWithoutQuery(
682
+ request.path ?? request.originalUrl ?? request.url
683
+ );
605
684
  const resolved = {
606
685
  headers
607
686
  };
@@ -621,15 +700,13 @@ export {
621
700
  TenantNotResolvedError,
622
701
  betterTenant,
623
702
  createTenantApi,
703
+ describeStrategies,
624
704
  getContext,
625
- getDatabase,
626
705
  handleRequest,
627
706
  resolveTenant,
628
- resolveTenantAsync,
629
707
  runAs,
630
708
  runAsSystem,
631
709
  runWithTenant,
632
- runWithTenantAndDatabase,
633
710
  sendCliTelemetry,
634
711
  toResolvableRequest
635
712
  };