najm-auth 4.0.4 → 4.0.6
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 +1301 -1286
- package/dist/client/edge.d.ts +3 -3
- package/dist/client/server/index.d.ts +5 -3
- package/dist/client/server/index.js +1 -0
- package/dist/identity/ma.d.ts +1 -1
- package/dist/identity/ma.js +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +48 -5
- package/dist/{ma-sNHnUGLO.d.ts → ma-Cu6xUMez.d.ts} +6 -1
- package/dist/schema/pg.js +6 -1
- package/dist/schema/sqlite.js +4 -1
- package/package.json +1 -1
package/dist/client/edge.d.ts
CHANGED
|
@@ -18,13 +18,13 @@ interface AuthProxyOptions {
|
|
|
18
18
|
}
|
|
19
19
|
interface AuthMiddlewareConfig {
|
|
20
20
|
/** Routes that require authentication (glob patterns) */
|
|
21
|
-
protectedRoutes?: string[];
|
|
21
|
+
protectedRoutes?: readonly string[];
|
|
22
22
|
/** Always-public routes (glob patterns) */
|
|
23
|
-
publicRoutes?: string[];
|
|
23
|
+
publicRoutes?: readonly string[];
|
|
24
24
|
/** Route to redirect unauthenticated users to */
|
|
25
25
|
loginRoute?: string;
|
|
26
26
|
/** Routes restricted to specific roles: { '/admin/*': ['admin'] } */
|
|
27
|
-
roleRoutes?: Record<string, string[]
|
|
27
|
+
roleRoutes?: Readonly<Record<string, readonly string[]>>;
|
|
28
28
|
/** Refresh token cookie name (default: 'refreshToken') */
|
|
29
29
|
cookieName?: string;
|
|
30
30
|
/** API base URL used by session recovery (default: '/api'). */
|
|
@@ -158,6 +158,8 @@ interface DefineAuthConfig {
|
|
|
158
158
|
authPrefix?: string;
|
|
159
159
|
/** Refresh token cookie name (default: 'refreshToken') */
|
|
160
160
|
cookieName?: string;
|
|
161
|
+
/** Remember Me preference cookie used by routeHandlers; per-handler options override it. */
|
|
162
|
+
rememberCookieName?: string;
|
|
161
163
|
/** Proactive refresh at this fraction of token lifetime (default: 0.8) */
|
|
162
164
|
refreshThreshold?: number;
|
|
163
165
|
/** Enable multi-tab sync via BroadcastChannel (default: true) */
|
|
@@ -183,11 +185,11 @@ interface DefineAuthConfig {
|
|
|
183
185
|
*/
|
|
184
186
|
forbiddenRoute?: string;
|
|
185
187
|
/** Routes that are always public (glob patterns) */
|
|
186
|
-
publicRoutes?: string[];
|
|
188
|
+
publicRoutes?: readonly string[];
|
|
187
189
|
/** Routes that require authentication (glob patterns) */
|
|
188
|
-
protectedRoutes?: string[];
|
|
190
|
+
protectedRoutes?: readonly string[];
|
|
189
191
|
/** Routes restricted to specific roles: { '/admin/:path*': ['admin'] } */
|
|
190
|
-
roleRoutes?: Record<string, string[]
|
|
192
|
+
roleRoutes?: Readonly<Record<string, readonly string[]>>;
|
|
191
193
|
/** Session cookie name (default: 'najm.session') */
|
|
192
194
|
sessionCookieName?: string;
|
|
193
195
|
/** Secret for verifying session cookie HMAC. Falls back to env vars. */
|
|
@@ -1734,6 +1734,7 @@ function defineAuth(authConfig = {}) {
|
|
|
1734
1734
|
const routeHandlers = /* @__PURE__ */ __name((handler, options = {}) => {
|
|
1735
1735
|
const persistentHandler = withAuthCookiePersistence(handler, {
|
|
1736
1736
|
...options,
|
|
1737
|
+
rememberCookieName: options.rememberCookieName ?? authConfig.rememberCookieName,
|
|
1737
1738
|
authCookieNames: options.authCookieNames ?? [cookieName2, sessionCookieName]
|
|
1738
1739
|
});
|
|
1739
1740
|
return {
|
package/dist/identity/ma.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { M as MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, e as TemporaryCredential, T as TemporaryCredentialInput, i as isMoroccanCin, m as moroccanCinTemporaryCredential, j as moroccoIdentityPreset, n as normalizeMoroccanCin, k as normalizeMoroccanPhone } from '../ma-
|
|
1
|
+
export { M as MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, e as TemporaryCredential, T as TemporaryCredentialInput, i as isMoroccanCin, m as moroccanCinTemporaryCredential, j as moroccoIdentityPreset, n as normalizeMoroccanCin, k as normalizeMoroccanPhone } from '../ma-Cu6xUMez.js';
|
package/dist/identity/ma.js
CHANGED
|
@@ -31,7 +31,7 @@ var MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
|
|
|
31
31
|
var MOROCCAN_CIN = /^[a-z]{1,3}\d{5,17}$/i;
|
|
32
32
|
function isMoroccanCin(value) {
|
|
33
33
|
const trimmed = value.trim();
|
|
34
|
-
return trimmed.length >=
|
|
34
|
+
return trimmed.length >= 7 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
|
|
35
35
|
}
|
|
36
36
|
__name(isMoroccanCin, "isMoroccanCin");
|
|
37
37
|
function normalizeMoroccanCin(value) {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { ValidationPluginConfig } from 'najm-validation';
|
|
|
4
4
|
import { RateLimitPluginConfig, RateLimitKeyContext, TimeWindow } from 'najm-rate';
|
|
5
5
|
import { CachePluginConfig, CacheService } from 'najm-cache';
|
|
6
6
|
import { EmailPluginConfig, EmailService } from 'najm-email';
|
|
7
|
-
import { R as ResolvedIdentityConfig, I as IdentityConfig, T as TemporaryCredentialInput } from './ma-
|
|
8
|
-
export { D as DEFAULT_IDENTITY_PRESET, E as EXACT_TEMPORARY_CREDENTIAL_KIND, a as IDENTITY_PRESETS, b as IdentityNormalizer, c as IdentityPreset, d as IdentityPresetName, M as MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, e as TemporaryCredential, f as TemporaryCredentialKind, g as compactPhone, i as isMoroccanCin, h as isTemporaryCredentialKind, m as moroccanCinTemporaryCredential, j as moroccoIdentityPreset, n as normalizeMoroccanCin, k as normalizeMoroccanPhone, l as normalizeTunisianPhone, r as resolveTemporaryCredentialKind, t as toTemporaryCredential, o as tunisiaIdentityPreset } from './ma-
|
|
7
|
+
import { R as ResolvedIdentityConfig, I as IdentityConfig, T as TemporaryCredentialInput } from './ma-Cu6xUMez.js';
|
|
8
|
+
export { D as DEFAULT_IDENTITY_PRESET, E as EXACT_TEMPORARY_CREDENTIAL_KIND, a as IDENTITY_PRESETS, b as IdentityNormalizer, c as IdentityPreset, d as IdentityPresetName, M as MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND, e as TemporaryCredential, f as TemporaryCredentialKind, g as compactPhone, i as isMoroccanCin, h as isTemporaryCredentialKind, m as moroccanCinTemporaryCredential, j as moroccoIdentityPreset, n as normalizeMoroccanCin, k as normalizeMoroccanPhone, l as normalizeTunisianPhone, r as resolveTemporaryCredentialKind, t as toTemporaryCredential, o as tunisiaIdentityPreset } from './ma-Cu6xUMez.js';
|
|
9
9
|
import { ZodType, z } from 'zod';
|
|
10
10
|
import { I18nService } from 'najm-i18n';
|
|
11
11
|
import { TDb, SeedEntry } from 'najm-database';
|
|
@@ -977,6 +977,13 @@ declare class RoleService {
|
|
|
977
977
|
updatedAt: string;
|
|
978
978
|
}[]>;
|
|
979
979
|
getRoleIdByName(name: any): Promise<string>;
|
|
980
|
+
/**
|
|
981
|
+
* checkNameUnique() reads before it writes, so two concurrent requests can
|
|
982
|
+
* both pass it and race to insert the same name. The database rejects the
|
|
983
|
+
* loser; answer it with the same 409 the validator would have given rather
|
|
984
|
+
* than leaking a raw driver error as a 500.
|
|
985
|
+
*/
|
|
986
|
+
private onDuplicateName;
|
|
980
987
|
}
|
|
981
988
|
|
|
982
989
|
declare class TokenRepository {
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,12 @@ var rolesTable = pgTable("roles", {
|
|
|
40
40
|
...baseFields(5),
|
|
41
41
|
name: text("name").notNull(),
|
|
42
42
|
description: text("description")
|
|
43
|
-
})
|
|
43
|
+
}, (table) => ({
|
|
44
|
+
// A role name is a domain identity: guards match `admin` by name, and seeding
|
|
45
|
+
// reconciles roles by name. Two rows sharing a name make authorization
|
|
46
|
+
// ambiguous, so the database — not a check-then-insert query — enforces it.
|
|
47
|
+
nameUnique: uniqueIndex("roles_name_unique").on(table.name)
|
|
48
|
+
}));
|
|
44
49
|
var usersTable = pgTable("users", {
|
|
45
50
|
...baseFields(8),
|
|
46
51
|
name: text("name"),
|
|
@@ -144,7 +149,10 @@ var rolesTable2 = sqliteTable("roles", {
|
|
|
144
149
|
...baseFields2(5),
|
|
145
150
|
name: text2("name").notNull(),
|
|
146
151
|
description: text2("description")
|
|
147
|
-
})
|
|
152
|
+
}, (table) => ({
|
|
153
|
+
// Same domain identity as the PostgreSQL dialect; see schema/pg.ts.
|
|
154
|
+
nameUnique: uniqueIndex2("roles_name_unique").on(table.name)
|
|
155
|
+
}));
|
|
148
156
|
var usersTable2 = sqliteTable("users", {
|
|
149
157
|
...baseFields2(8),
|
|
150
158
|
name: text2("name"),
|
|
@@ -1096,7 +1104,7 @@ var RoleService = class RoleService2 {
|
|
|
1096
1104
|
}
|
|
1097
1105
|
async create(data) {
|
|
1098
1106
|
await this.roleValidator.checkNameUnique(data.name);
|
|
1099
|
-
return await this.roleRepository.create(data);
|
|
1107
|
+
return await this.onDuplicateName(() => this.roleRepository.create(data));
|
|
1100
1108
|
}
|
|
1101
1109
|
async update(id, data) {
|
|
1102
1110
|
const role = await this.roleValidator.checkRoleExists(id);
|
|
@@ -1104,7 +1112,7 @@ var RoleService = class RoleService2 {
|
|
|
1104
1112
|
Err3(this.t("errors.cannotRenameSystem"), 403);
|
|
1105
1113
|
}
|
|
1106
1114
|
await this.roleValidator.checkNameUnique(data.name, id);
|
|
1107
|
-
return await this.roleRepository.update(id, data);
|
|
1115
|
+
return await this.onDuplicateName(() => this.roleRepository.update(id, data));
|
|
1108
1116
|
}
|
|
1109
1117
|
async delete(id) {
|
|
1110
1118
|
const role = await this.roleValidator.checkRoleExists(id);
|
|
@@ -1131,6 +1139,22 @@ var RoleService = class RoleService2 {
|
|
|
1131
1139
|
const role = await this.getByName(name);
|
|
1132
1140
|
return role?.id;
|
|
1133
1141
|
}
|
|
1142
|
+
/**
|
|
1143
|
+
* checkNameUnique() reads before it writes, so two concurrent requests can
|
|
1144
|
+
* both pass it and race to insert the same name. The database rejects the
|
|
1145
|
+
* loser; answer it with the same 409 the validator would have given rather
|
|
1146
|
+
* than leaking a raw driver error as a 500.
|
|
1147
|
+
*/
|
|
1148
|
+
async onDuplicateName(write) {
|
|
1149
|
+
try {
|
|
1150
|
+
return await write();
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
if (isDuplicateRoleName(error)) {
|
|
1153
|
+
Err3(this.t("errors.exists"), 409);
|
|
1154
|
+
}
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1134
1158
|
};
|
|
1135
1159
|
__decorate7([
|
|
1136
1160
|
I18n3("roles"),
|
|
@@ -1140,6 +1164,18 @@ RoleService = __decorate7([
|
|
|
1140
1164
|
Injectable4(),
|
|
1141
1165
|
__metadata7("design:paramtypes", [typeof (_a4 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a4 : Object, typeof (_b2 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _b2 : Object])
|
|
1142
1166
|
], RoleService);
|
|
1167
|
+
function isDuplicateRoleName(error) {
|
|
1168
|
+
if (!error || typeof error !== "object")
|
|
1169
|
+
return false;
|
|
1170
|
+
const candidate = error;
|
|
1171
|
+
if (candidate.code === "23505")
|
|
1172
|
+
return true;
|
|
1173
|
+
if (candidate.code === "SQLITE_CONSTRAINT_UNIQUE")
|
|
1174
|
+
return true;
|
|
1175
|
+
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
1176
|
+
return /unique constraint failed/i.test(message) || /duplicate key value violates unique constraint/i.test(message);
|
|
1177
|
+
}
|
|
1178
|
+
__name(isDuplicateRoleName, "isDuplicateRoleName");
|
|
1143
1179
|
|
|
1144
1180
|
// src/users/UserService.ts
|
|
1145
1181
|
import { nanoid as nanoid3 } from "nanoid";
|
|
@@ -2777,7 +2813,7 @@ var MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
|
|
|
2777
2813
|
var MOROCCAN_CIN = /^[a-z]{1,3}\d{5,17}$/i;
|
|
2778
2814
|
function isMoroccanCin(value) {
|
|
2779
2815
|
const trimmed = value.trim();
|
|
2780
|
-
return trimmed.length >=
|
|
2816
|
+
return trimmed.length >= 7 && trimmed.length <= 20 && MOROCCAN_CIN.test(trimmed);
|
|
2781
2817
|
}
|
|
2782
2818
|
__name(isMoroccanCin, "isMoroccanCin");
|
|
2783
2819
|
function normalizeMoroccanCin(value) {
|
|
@@ -7651,6 +7687,13 @@ var toSeedId = /* @__PURE__ */ __name((prefix, value) => {
|
|
|
7651
7687
|
var authSeed = /* @__PURE__ */ __name((config) => ({
|
|
7652
7688
|
roles: {
|
|
7653
7689
|
schema: createRoleDto,
|
|
7690
|
+
// Roles are identified by name, not by the proposed primary key. Without
|
|
7691
|
+
// this the seeder falls back to the primary key, and a role that already
|
|
7692
|
+
// exists under a legacy/random ID is inserted a second time under the
|
|
7693
|
+
// deterministic one. Query-back by name then hands downstream resolvers the
|
|
7694
|
+
// live row, so users and role_permissions reference the ID the database
|
|
7695
|
+
// actually holds — never a proposed ID that was skipped on conflict.
|
|
7696
|
+
by: ["name"],
|
|
7654
7697
|
rows: (config.roles ?? [
|
|
7655
7698
|
{ name: "admin", description: "System administrator with full access" },
|
|
7656
7699
|
{ name: "user", description: "Regular user with limited access" }
|
|
@@ -66,7 +66,12 @@ interface TemporaryCredentialKind {
|
|
|
66
66
|
/** Default kind: byte-for-byte, case-sensitive, no transformation. */
|
|
67
67
|
declare const EXACT_TEMPORARY_CREDENTIAL_KIND = "exact";
|
|
68
68
|
declare const MOROCCAN_CIN_TEMPORARY_CREDENTIAL_KIND = "ma-cin";
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* A Moroccan CIN is 1–3 letters followed by digits, 7–20 characters overall.
|
|
71
|
+
* The floor is 7, not 8: the issued formats are one or two letters followed
|
|
72
|
+
* by five or six digits, so `BB46123` and `A123456` are real cards that an
|
|
73
|
+
* 8 floor turned away.
|
|
74
|
+
*/
|
|
70
75
|
declare function isMoroccanCin(value: string): boolean;
|
|
71
76
|
/**
|
|
72
77
|
* People type their CIN in either case. Anything that is not a CIN is left
|
package/dist/schema/pg.js
CHANGED
|
@@ -24,7 +24,12 @@ var rolesTable = pgTable("roles", {
|
|
|
24
24
|
...baseFields(5),
|
|
25
25
|
name: text("name").notNull(),
|
|
26
26
|
description: text("description")
|
|
27
|
-
})
|
|
27
|
+
}, (table) => ({
|
|
28
|
+
// A role name is a domain identity: guards match `admin` by name, and seeding
|
|
29
|
+
// reconciles roles by name. Two rows sharing a name make authorization
|
|
30
|
+
// ambiguous, so the database — not a check-then-insert query — enforces it.
|
|
31
|
+
nameUnique: uniqueIndex("roles_name_unique").on(table.name)
|
|
32
|
+
}));
|
|
28
33
|
var usersTable = pgTable("users", {
|
|
29
34
|
...baseFields(8),
|
|
30
35
|
name: text("name"),
|
package/dist/schema/sqlite.js
CHANGED
|
@@ -14,7 +14,10 @@ var rolesTable = sqliteTable("roles", {
|
|
|
14
14
|
...baseFields(5),
|
|
15
15
|
name: text("name").notNull(),
|
|
16
16
|
description: text("description")
|
|
17
|
-
})
|
|
17
|
+
}, (table) => ({
|
|
18
|
+
// Same domain identity as the PostgreSQL dialect; see schema/pg.ts.
|
|
19
|
+
nameUnique: uniqueIndex("roles_name_unique").on(table.name)
|
|
20
|
+
}));
|
|
18
21
|
var usersTable = sqliteTable("users", {
|
|
19
22
|
...baseFields(8),
|
|
20
23
|
name: text("name"),
|