najm-auth 4.0.8 → 4.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/README.md CHANGED
@@ -508,44 +508,36 @@ export const Product = own(products)
508
508
  join(products.userId, _users.id),
509
509
  where(_users.id)
510
510
  )
511
- .writeBy(products.userId); // Enforce on create/update
511
+ .writeBy(products.userId); // Metadata for consumers; not an automatic write guard
512
512
  ```
513
513
 
514
514
  ### Using @Policy and @Owned
515
515
 
516
516
  ```typescript
517
- import { configureOwnership, Policy, CanList, CanRead, CanCreate, CanUpdate, CanDelete } from 'najm-auth';
518
-
519
- const config = configureOwnership({
520
- adminRoles: ['admin'],
521
- rules: {
522
- 'user': {
523
- 'products': Product.getRules()['user']
524
- }
525
- }
526
- });
517
+ import { Owned, Policy, CanList, CanRead, CanCreate, CanUpdate, CanDelete } from 'najm-auth';
518
+ import { and, eq, type SQL } from 'drizzle-orm';
527
519
 
528
520
  @Policy(Product)
529
521
  @Controller('/api/products')
530
522
  export class ProductController {
531
523
  @Get('/')
532
- @CanList() // List only owned products
533
- getAll(@GuardParams() filter: any) { }
524
+ @CanList() // Checks read permission; repository scopes rows
525
+ getAll() { /* delegate to a scoped service/repository read */ }
534
526
 
535
527
  @Get('/:id')
536
- @CanRead() // Read only if owner
528
+ @CanRead() // Checks read permission; lookup must scope the ID
537
529
  getOne() { }
538
530
 
539
531
  @Post('/')
540
- @CanCreate() // Create (ownership assigned automatically)
532
+ @CanCreate() // Service must validate/assign ownership
541
533
  create(@Body() data: any) { }
542
534
 
543
535
  @Put('/:id')
544
- @CanUpdate() // Update only if owner
536
+ @CanUpdate() // Service must load an owned record before writing
545
537
  update(@Body() data: any) { }
546
538
 
547
539
  @Delete('/:id')
548
- @CanDelete() // Delete only if owner
540
+ @CanDelete() // Service must load an owned record before deleting
549
541
  delete() { }
550
542
  }
551
543
 
@@ -554,21 +546,54 @@ export class ProductController {
554
546
  export class ProductRepository {
555
547
  @DB() db!: Database;
556
548
 
557
- // Auto-scoped to current user
558
- async findMany(opts?: { where?: any; limit?: number }) {
559
- return this.findMany(opts); // Only returns owned products
560
- }
561
-
562
- async findOne(opts: { where: any }) {
563
- return this.findOne(opts); // Returns null if not owned
564
- }
549
+ // These methods are supplied by @Owned; do not define recursive wrappers.
550
+ declare ownershipCondition: () => SQL | undefined;
551
+ declare findMany: (opts?: { where?: SQL; limit?: number }) => Promise<ProductRow[]>;
552
+ declare findOne: (opts: { where: SQL }) => Promise<ProductRow | null>;
565
553
 
566
- async scopedQuery() {
567
- return this.scopedQuery(); // Raw scoped query builder
554
+ async getById(id: string) {
555
+ const [row] = await this.db.select().from(products)
556
+ .where(and(this.ownershipCondition(), eq(products.id, id))).limit(1);
557
+ return row ?? null;
568
558
  }
569
559
  }
570
560
  ```
571
561
 
562
+ ### Composable ownership (4.1+)
563
+
564
+ `@Owned(Product)` now provides `ownershipCondition()` for custom queries.
565
+ It returns a parameterized `id IN (authorized IDs)` predicate; join-based rules
566
+ do not multiply outer rows. Combine it with filters in one
567
+ `.where(and(this.ownershipCondition(), filter))`. The owned table must have an
568
+ `id` column. A standalone `ownershipCondition(db, tokens, context)` and the
569
+ `OwnershipReadContext` / `OwnershipConditionMethods` types are also exported.
570
+
571
+ `@Owned(Product, SharedProduct)` grants either rule (OR), then your query's
572
+ filters apply (AND). Every alternative must reference the same table object.
573
+ Alternatives work with `ownershipCondition`, `findMany`, `findOne`, and
574
+ `scopedQuery`. Single-token helpers retain their existing behavior.
575
+
576
+ Configure school-wide, tenant-wide, or other privileged roles in the app:
577
+
578
+ ```typescript
579
+ const Product = own(products, { adminRoles: ['admin', 'operations'] })
580
+ .for('member', where(products.userId));
581
+ ```
582
+
583
+ Those roles bypass row ownership only; route permissions still apply. Unknown
584
+ roles and anonymous active requests get no rows. Outside request context,
585
+ seed/job reads remain unscoped for compatibility; never treat a manually
586
+ constructed repository without request injection as an authenticated boundary.
587
+
588
+ **Migrating custom queries:** replace `this.scope(query).where(filter)` with
589
+ `query.where(and(this.ownershipCondition(), filter))`. Drizzle replaces an
590
+ earlier WHERE when another `.where()` is called. The legacy `scope()` and
591
+ `scopedQuery()` APIs remain for compatibility, but their result must not be
592
+ followed by `.where()`; for simple reads use `findMany({ where: filter })`.
593
+ An upgrade alone does not rewrite unsafe queries in consumers. Keep role
594
+ and cross-user integration tests in each app. `@Policy` and `@CanRead` check
595
+ permissions, not the ownership of a requested record ID.
596
+
572
597
  ### Advanced Ownership: Multi-Role Scoping
573
598
 
574
599
  ```typescript
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export { CredentialSetupRequirement, CredentialSetupSession, NewCredentialSetupR
14
14
  import { CookieService } from 'najm-cookies';
15
15
  import { Context } from 'hono';
16
16
  import { GuardResult } from 'najm-guard';
17
- import 'drizzle-orm';
17
+ import { SQL } from 'drizzle-orm';
18
18
  import 'drizzle-orm/pg-core';
19
19
 
20
20
  interface CredentialSetupOptions {
@@ -2856,7 +2856,29 @@ declare class ScopeContext {
2856
2856
  role: string;
2857
2857
  } | null;
2858
2858
  }
2859
- declare function Owned(token: OwnershipToken): (target: Function) => void;
2859
+ declare function Owned(token: OwnershipToken, ...alternatives: OwnershipToken[]): (target: Function) => void;
2860
+
2861
+ /** Request context consumed by ownershipCondition; ScopeContext implements it. */
2862
+ interface OwnershipReadContext {
2863
+ hasActiveContext(): boolean;
2864
+ getUser(): {
2865
+ id: string;
2866
+ role: string;
2867
+ } | null;
2868
+ }
2869
+ /** Additional custom-query method installed by @Owned. */
2870
+ interface OwnershipConditionMethods {
2871
+ ownershipCondition(): SQL | undefined;
2872
+ }
2873
+ /**
2874
+ * Compose this predicate with application filters in ONE .where(and(...)).
2875
+ * Each token grants alternative access to the same table through an ID
2876
+ * subquery, so joins cannot duplicate outer rows or collide with outer joins.
2877
+ * The table must expose an `id` column. Roles and ownership rules remain
2878
+ * application-owned. Unknown roles and anonymous active requests deny all.
2879
+ * Outside a request, reads remain unscoped for seed/job compatibility.
2880
+ */
2881
+ declare function ownershipCondition(db: any, tokens: readonly OwnershipToken[], context: OwnershipReadContext | undefined): SQL | undefined;
2860
2882
 
2861
2883
  /**
2862
2884
  * Shared query builders used across auth repositories.
@@ -3181,4 +3203,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
3181
3203
  */
3182
3204
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
3183
3205
 
3184
- export { AUTH_CONFIG, AUTH_CORE_MODULE, en as AUTH_EN, AUTH_LOCALES, AUTH_LOGIN_RATE_LIMIT_ENV, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AdministrativeDelivery, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthLoginRateLimitConfig, type AuthPluginConfig, AuthQueries, type AuthRateLimitEnvironment, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, CREDENTIAL_SETUP_CODES, CREDENTIAL_SETUP_MODULE, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, type ConsumedSetPasswordToken, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type CredentialSetupChangeDto, type CredentialSetupCode, type CredentialSetupConfig, CredentialSetupController, type CredentialSetupOptions, type CredentialSetupPasswordOptions, type CredentialSetupPending, CredentialSetupRepository, CredentialSetupRequirementRepository, type CredentialSetupRequirementRow, CredentialSetupRequirementService, CredentialSetupService, type CredentialSetupSessionInfo, type CredentialSetupStarted, DEFAULT_AUTH_LOGIN_RATE_LIMIT, DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME, DEFAULT_CREDENTIAL_SETUP_TTL_MS, type DefineRolesOptions, type EmailParam, EncryptionService, type GitHubOAuthConfig, type GoogleOAuthConfig, IdentityConfig, type IdentityResolver, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, type LoginResult, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, PASSWORD_SETUP_PURPOSE, PUBLIC_REGISTRATION_MODULE, PasswordSetupService, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, type ProvisionUserWithPasswordInput, type ProvisionUserWithSetupInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, RegistrationController, type ResetPasswordDto, type ResolvedCredentialSetupConfig, ResolvedIdentityConfig, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, SessionInvalidationService, type SetPasswordToken, type SetPasswordTokenType, TOKEN_STATUS, TOKEN_TYPE, TemporaryCredentialInput, type TemporaryCredentialReset, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authEmailRateLimitKey, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createIdentityResolver, createPermissionDto, createRoleDto, createTokenDto, createUserDto, credentialSetupChangeDto, credentialSetupError, defaultCredentialSetupPasswordSchema, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, normalizeSetupPurpose, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, resolveAuthLoginRateLimitConfig, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
3206
+ export { AUTH_CONFIG, AUTH_CORE_MODULE, en as AUTH_EN, AUTH_LOCALES, AUTH_LOGIN_RATE_LIMIT_ENV, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AdministrativeDelivery, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthLoginRateLimitConfig, type AuthPluginConfig, AuthQueries, type AuthRateLimitEnvironment, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, CREDENTIAL_SETUP_CODES, CREDENTIAL_SETUP_MODULE, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, type ConsumedSetPasswordToken, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type CredentialSetupChangeDto, type CredentialSetupCode, type CredentialSetupConfig, CredentialSetupController, type CredentialSetupOptions, type CredentialSetupPasswordOptions, type CredentialSetupPending, CredentialSetupRepository, CredentialSetupRequirementRepository, type CredentialSetupRequirementRow, CredentialSetupRequirementService, CredentialSetupService, type CredentialSetupSessionInfo, type CredentialSetupStarted, DEFAULT_AUTH_LOGIN_RATE_LIMIT, DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME, DEFAULT_CREDENTIAL_SETUP_TTL_MS, type DefineRolesOptions, type EmailParam, EncryptionService, type GitHubOAuthConfig, type GoogleOAuthConfig, IdentityConfig, type IdentityResolver, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, type LoginResult, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnershipConditionMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipReadContext, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, PASSWORD_SETUP_PURPOSE, PUBLIC_REGISTRATION_MODULE, PasswordSetupService, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, type ProvisionUserWithPasswordInput, type ProvisionUserWithSetupInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, RegistrationController, type ResetPasswordDto, type ResolvedCredentialSetupConfig, ResolvedIdentityConfig, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, SessionInvalidationService, type SetPasswordToken, type SetPasswordTokenType, TOKEN_STATUS, TOKEN_TYPE, TemporaryCredentialInput, type TemporaryCredentialReset, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authEmailRateLimitKey, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createIdentityResolver, createPermissionDto, createRoleDto, createTokenDto, createUserDto, credentialSetupChangeDto, credentialSetupError, defaultCredentialSetupPasswordSchema, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, normalizeSetupPurpose, own, ownershipCondition, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, resolveAuthLoginRateLimitConfig, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
package/dist/index.js CHANGED
@@ -1993,9 +1993,7 @@ var UserService = class UserService2 {
1993
1993
  if (!config?.email || !config?.password) {
1994
1994
  Err5("Admin email and password must be provided via config parameter");
1995
1995
  }
1996
- if (config.password.length < 12) {
1997
- Err5("Admin password must be at least 12 characters");
1998
- }
1996
+ this.userValidator.validatePasswordStrength(config.password);
1999
1997
  const adminRole = await this.roleValidator.checkAdminRoleExists();
2000
1998
  const existingUser = await this.userRepository.getByEmail(config.email);
2001
1999
  if (existingUser) {
@@ -5973,7 +5971,7 @@ var OwnershipToken = class {
5973
5971
  symbol;
5974
5972
  name;
5975
5973
  table;
5976
- _rules = {};
5974
+ _rules = /* @__PURE__ */ Object.create(null);
5977
5975
  _writeScopeCol;
5978
5976
  _adminRoles;
5979
5977
  constructor(table, opts) {
@@ -6385,9 +6383,47 @@ __name(Policy, "Policy");
6385
6383
 
6386
6384
  // src/ownership/OwnedDecorator.ts
6387
6385
  import "reflect-metadata";
6388
- import { sql as sql5, and as and5 } from "drizzle-orm";
6386
+ import { sql as sql6, and as and5 } from "drizzle-orm";
6389
6387
  import { Injectable as Injectable17, Inject as Inject17, DI as DI3, Container as Container2, REQUEST_ID } from "najm-core";
6390
6388
  import { USER as USER2 } from "najm-guard";
6389
+
6390
+ // src/ownership/ownershipCondition.ts
6391
+ import { inArray, or, sql as sql5 } from "drizzle-orm";
6392
+ function validateOwnershipAlternatives(tokens) {
6393
+ if (!tokens.length)
6394
+ throw new Error("Ownership requires at least one token");
6395
+ const table = tokens[0].table;
6396
+ if (tokens.some((token) => token.table !== table)) {
6397
+ throw new Error("Ownership alternatives must refer to the same table");
6398
+ }
6399
+ }
6400
+ __name(validateOwnershipAlternatives, "validateOwnershipAlternatives");
6401
+ function ownershipCondition(db, tokens, context) {
6402
+ validateOwnershipAlternatives(tokens);
6403
+ const idColumn = tokens[0].table.id;
6404
+ if (!idColumn)
6405
+ throw new Error("ownershipCondition requires a table with an id column");
6406
+ if (!context?.hasActiveContext())
6407
+ return void 0;
6408
+ const user = context.getUser();
6409
+ if (!user)
6410
+ return sql5`1 = 0`;
6411
+ const conditions = [];
6412
+ for (const token of tokens) {
6413
+ const { query, condition } = token.applyScopeSplit(user.id, user.role, db.select({ id: idColumn }).from(token.table));
6414
+ if (condition === null)
6415
+ return void 0;
6416
+ if (!Object.prototype.hasOwnProperty.call(token.getRules(), user.role))
6417
+ continue;
6418
+ conditions.push(inArray(idColumn, query.where(condition)));
6419
+ }
6420
+ if (!conditions.length)
6421
+ return sql5`1 = 0`;
6422
+ return conditions.length === 1 ? conditions[0] : or(...conditions);
6423
+ }
6424
+ __name(ownershipCondition, "ownershipCondition");
6425
+
6426
+ // src/ownership/OwnedDecorator.ts
6391
6427
  var __decorate33 = function(decorators, target, key, desc) {
6392
6428
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
6393
6429
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -6436,11 +6472,20 @@ __decorate33([
6436
6472
  ScopeContext = __decorate33([
6437
6473
  Injectable17()
6438
6474
  ], ScopeContext);
6439
- function Owned(token) {
6475
+ function Owned(token, ...alternatives) {
6476
+ const tokens = [token, ...alternatives];
6477
+ validateOwnershipAlternatives(tokens);
6440
6478
  return function(target) {
6441
6479
  Reflect.defineMetadata(OWNED_META, token, target);
6442
6480
  const proto = target.prototype;
6443
6481
  Inject17(ScopeContext)(proto, "_scopeCtx");
6482
+ Object.defineProperty(proto, "ownershipCondition", {
6483
+ value() {
6484
+ return ownershipCondition(this.db, tokens, this._scopeCtx);
6485
+ },
6486
+ configurable: true,
6487
+ enumerable: false
6488
+ });
6444
6489
  function getUser(self) {
6445
6490
  return self._scopeCtx?.getUser() ?? null;
6446
6491
  }
@@ -6472,9 +6517,11 @@ function Owned(token) {
6472
6517
  return (query) => {
6473
6518
  if (!hasActiveContext(this))
6474
6519
  return query;
6520
+ if (alternatives.length)
6521
+ return query.where(this.ownershipCondition());
6475
6522
  const user = getUser(this);
6476
6523
  if (!user)
6477
- return query.where(sql5`1 = 0`);
6524
+ return query.where(sql6`1 = 0`);
6478
6525
  return token.applyScope(user.id, user.role, query);
6479
6526
  };
6480
6527
  },
@@ -6491,6 +6538,9 @@ function Owned(token) {
6491
6538
  if (!user)
6492
6539
  return [];
6493
6540
  const base = this.db.select().from(token.table);
6541
+ if (alternatives.length) {
6542
+ return applyQueryOptions(base, opts, combineConditions(this.ownershipCondition(), opts.where));
6543
+ }
6494
6544
  const { query: q, condition: scopeCondition } = token.applyScopeSplit(user.id, user.role, base);
6495
6545
  return applyQueryOptions(q, opts, combineConditions(scopeCondition, opts.where));
6496
6546
  };
@@ -8115,6 +8165,7 @@ export {
8115
8165
  normalizeTunisianPhone,
8116
8166
  oauthAccountsTable,
8117
8167
  own,
8168
+ ownershipCondition,
8118
8169
  parseSchema,
8119
8170
  permissionIdParam,
8120
8171
  permissionsTable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "4.0.8",
3
+ "version": "4.1.0",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [