@mostajs/auth 2.3.3 → 2.5.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.
@@ -0,0 +1,3 @@
1
+ export declare function defaultStatusCheck(user: any): boolean;
2
+ export declare function defaultRoleResolver(user: any, fallback: string): string;
3
+ export declare function defaultNameResolver(user: any): string;
@@ -0,0 +1,23 @@
1
+ // @mostajs/auth — Helpers partagés entre credentials providers (local, remote, server)
2
+ //
3
+ // Centralise les valeurs par défaut pour : check de status, résolution
4
+ // de role, résolution de display name. Utilisé par :
5
+ // - credentials-provider.ts (NextAuth provider local, ORM direct)
6
+ // - credentials-verify.ts (handler serveur côté Octonet)
7
+ // - remote-credentials-provider.ts (NextAuth provider gateway, ne fait
8
+ // pas de bcrypt — délègue au handler)
9
+ //
10
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
11
+ export function defaultStatusCheck(user) {
12
+ return user.status !== 'disabled' && user.status !== 'locked';
13
+ }
14
+ export function defaultRoleResolver(user, fallback) {
15
+ if (user.roles?.length > 0) {
16
+ const r = user.roles[0];
17
+ return typeof r === 'string' ? r : (r?.name ?? fallback);
18
+ }
19
+ return user.role ?? fallback;
20
+ }
21
+ export function defaultNameResolver(user) {
22
+ return `${user.firstName ?? ''} ${user.lastName ?? ''}`.trim() || (user.email ?? '');
23
+ }
@@ -0,0 +1,57 @@
1
+ export interface CredentialsProviderConfig {
2
+ /** Lookup the user row by email (case-insensitive). Return null if not found.
3
+ * Conseillé : passer une fonction qui utilise UserRepository de @mostajs/rbac
4
+ * pour bénéficier de findByEmail (qui lowercase l'email côté repo). */
5
+ findUserByEmail: (email: string) => Promise<any | null>;
6
+ /** Default role assigned when the user has no role relations (default: 'user'). */
7
+ defaultRole?: string;
8
+ /** Custom check before allowing login (e.g. email verified, 2FA).
9
+ * Return false to block. Default: status !== 'disabled' && status !== 'locked'. */
10
+ checkUserAllowed?: (user: any) => boolean | Promise<boolean>;
11
+ /** Custom role resolution. Default: first user.roles[].name or fallback. */
12
+ resolveRole?: (user: any) => string | Promise<string>;
13
+ /** Custom display name. Default: "<firstName> <lastName>" trimmed → email. */
14
+ resolveName?: (user: any) => string;
15
+ /** Provider id (default: 'credentials'). Useful if multiple providers coexist. */
16
+ id?: string;
17
+ /** Provider display name (default: 'Email + Password'). */
18
+ name?: string;
19
+ }
20
+ /**
21
+ * Build a NextAuth Credentials provider config (returned as plain object,
22
+ * directly passable to `NextAuth({ providers: [provider], ... })` without
23
+ * any next-auth imports côté consumer).
24
+ *
25
+ * Usage :
26
+ * import { createCredentialsProvider } from '@mostajs/auth/server'
27
+ *
28
+ * NextAuth({
29
+ * providers: [
30
+ * createCredentialsProvider({
31
+ * findUserByEmail: async (email) => userRepo.findByEmail(email),
32
+ * defaultRole: 'user',
33
+ * }),
34
+ * ],
35
+ * })
36
+ */
37
+ export declare function createCredentialsProvider(config: CredentialsProviderConfig): {
38
+ id: string;
39
+ name: string;
40
+ type: "credentials";
41
+ credentials: {
42
+ email: {
43
+ label: string;
44
+ type: string;
45
+ };
46
+ password: {
47
+ label: string;
48
+ type: string;
49
+ };
50
+ };
51
+ authorize(credentials: any): Promise<{
52
+ id: any;
53
+ email: any;
54
+ name: string;
55
+ role: string;
56
+ } | null>;
57
+ };
@@ -0,0 +1,72 @@
1
+ // @mostajs/auth — Email + password credentials provider for NextAuth
2
+ //
3
+ // Factorise le boilerplate NextAuth Credentials que chaque app refait. Le
4
+ // consumer obtient un provider PRÊT À L'EMPLOI à passer à NextAuth({providers}) :
5
+ // il n'importe ni `next-auth/providers/credentials`, ni bcrypt, ni le
6
+ // UserRepository — tout est encapsulé.
7
+ //
8
+ // Pendant naturel de createApiKeyProvider (même style de retour : objet
9
+ // provider config plat). Différence : authentication via email/password
10
+ // (UI register/signin) vs API key (programmatic).
11
+ //
12
+ // Le caller fournit `findUserByEmail` (ou laisse passer pour un default
13
+ // fail-closed). Pour leverager directement @mostajs/rbac, il suffit de
14
+ // passer `(email) => new UserRepository(dialect).findByEmail(email)`.
15
+ //
16
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
17
+ import { comparePassword } from './password.js';
18
+ import { defaultStatusCheck, defaultRoleResolver, defaultNameResolver } from './credentials-helpers.js';
19
+ /**
20
+ * Build a NextAuth Credentials provider config (returned as plain object,
21
+ * directly passable to `NextAuth({ providers: [provider], ... })` without
22
+ * any next-auth imports côté consumer).
23
+ *
24
+ * Usage :
25
+ * import { createCredentialsProvider } from '@mostajs/auth/server'
26
+ *
27
+ * NextAuth({
28
+ * providers: [
29
+ * createCredentialsProvider({
30
+ * findUserByEmail: async (email) => userRepo.findByEmail(email),
31
+ * defaultRole: 'user',
32
+ * }),
33
+ * ],
34
+ * })
35
+ */
36
+ export function createCredentialsProvider(config) {
37
+ const defaultRole = config.defaultRole ?? 'user';
38
+ const checkUserAllowed = config.checkUserAllowed ?? defaultStatusCheck;
39
+ const resolveRole = config.resolveRole ?? ((u) => defaultRoleResolver(u, defaultRole));
40
+ const resolveName = config.resolveName ?? defaultNameResolver;
41
+ return {
42
+ id: config.id ?? 'credentials',
43
+ name: config.name ?? 'Email + Password',
44
+ type: 'credentials',
45
+ credentials: {
46
+ email: { label: 'Email', type: 'email' },
47
+ password: { label: 'Password', type: 'password' },
48
+ },
49
+ async authorize(credentials) {
50
+ if (!credentials?.email || !credentials?.password)
51
+ return null;
52
+ const email = String(credentials.email).toLowerCase().trim();
53
+ const user = await config.findUserByEmail(email);
54
+ if (!user)
55
+ return null;
56
+ const allowed = await checkUserAllowed(user);
57
+ if (!allowed)
58
+ return null;
59
+ const valid = await comparePassword(String(credentials.password), user.password);
60
+ if (!valid)
61
+ return null;
62
+ const role = await resolveRole(user);
63
+ const name = resolveName(user);
64
+ return {
65
+ id: user.id,
66
+ email: user.email,
67
+ name,
68
+ role,
69
+ };
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,36 @@
1
+ export interface CredentialsVerifyConfig {
2
+ /** Lookup the user row by email (case-insensitive) — must return the user
3
+ * WITH the password hash field. Typically:
4
+ * `(email) => new UserRepository(dialect).findByEmail(email)`
5
+ * Le handler tournant côté serveur (Octonet), il bypasse le sanitizer
6
+ * qui ne strippe que les responses sortant par REST. */
7
+ findUserByEmail: (email: string) => Promise<any | null>;
8
+ /** Default role assigned when the user has no role relations (default: 'user'). */
9
+ defaultRole?: string;
10
+ /** Custom check before allowing login. Default: status !== 'disabled' && !== 'locked'. */
11
+ checkUserAllowed?: (user: any) => boolean | Promise<boolean>;
12
+ /** Custom role resolution. Default: first user.roles[].name or fallback. */
13
+ resolveRole?: (user: any) => string | Promise<string>;
14
+ /** Custom display name. Default: "<firstName> <lastName>" trimmed → email. */
15
+ resolveName?: (user: any) => string;
16
+ }
17
+ /**
18
+ * Build a Web standard Request → Response handler that verifies (email, password)
19
+ * server-side and returns a sanitized user payload (no password) on success.
20
+ *
21
+ * Mount it on any HTTP framework that exposes Web Request/Response :
22
+ * Next.js : export const POST = createCredentialsVerifyHandler({...})
23
+ * Fastify : adapter via app.post(path, async (req,reply)=>{ const r = await handler(toRequest(req)); ... })
24
+ * mosta-net : monté automatiquement par le server quand auth est activé
25
+ *
26
+ * Response shape :
27
+ * 200 { ok: true, user: { id, email, name, role } }
28
+ * 400 { error: 'email and password are required' }
29
+ * 401 { error: 'invalid credentials' } // user not found, password wrong, status disabled
30
+ * 500 { error: 'internal error' }
31
+ *
32
+ * Le handler ne renvoie JAMAIS le password hash, ni d'indication sur quelle
33
+ * partie a échoué (user inexistant vs password faux) — défense contre
34
+ * l'énumération d'emails.
35
+ */
36
+ export declare function createCredentialsVerifyHandler(config: CredentialsVerifyConfig): (req: Request) => Promise<Response>;
@@ -0,0 +1,96 @@
1
+ // @mostajs/auth — Server-side credentials verification handler
2
+ //
3
+ // Frère symétrique de createCredentialsProvider. Là où celui-ci tourne
4
+ // CÔTÉ CLIENT NextAuth (Octocloud) en faisant bcrypt.compare localement,
5
+ // celui-ci tourne CÔTÉ SERVEUR (Octonet, là où vit la DB) et expose le
6
+ // même contrat sous forme de Web standard handler.
7
+ //
8
+ // Pourquoi ce frère existe :
9
+ // En mode gateway (data-plug → MOSTA_DATA=net), Octocloud n'a plus
10
+ // accès direct au password hash — le sanitizer middleware d'Octonet
11
+ // strippe `password` de toute response REST (à juste titre, c'est sa
12
+ // fonction de défense). Faire le bcrypt.compare côté Octocloud est
13
+ // alors impossible.
14
+ //
15
+ // La résolution propre : déplacer le bcrypt.compare LÀ où vit le hash
16
+ // (Octonet), et exposer juste un endpoint POST /api/auth/verify qui
17
+ // prend (email, password) et renvoie {ok:true, user:{id,email,name,role}}
18
+ // ou 401. Le hash ne quitte JAMAIS Octonet — propriété de sécurité forte.
19
+ //
20
+ // Côté Octocloud, on monte createRemoteCredentialsProvider qui POST
21
+ // sur cet endpoint au lieu de tenter le compare local.
22
+ //
23
+ // Auteur des deux côtés : @mostajs/auth (un seul module, deux rôles
24
+ // symétriques). @mostajs/net ne fait que router le HTTP.
25
+ //
26
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
27
+ import { comparePassword } from './password.js';
28
+ import { defaultStatusCheck, defaultRoleResolver, defaultNameResolver } from './credentials-helpers.js';
29
+ /**
30
+ * Build a Web standard Request → Response handler that verifies (email, password)
31
+ * server-side and returns a sanitized user payload (no password) on success.
32
+ *
33
+ * Mount it on any HTTP framework that exposes Web Request/Response :
34
+ * Next.js : export const POST = createCredentialsVerifyHandler({...})
35
+ * Fastify : adapter via app.post(path, async (req,reply)=>{ const r = await handler(toRequest(req)); ... })
36
+ * mosta-net : monté automatiquement par le server quand auth est activé
37
+ *
38
+ * Response shape :
39
+ * 200 { ok: true, user: { id, email, name, role } }
40
+ * 400 { error: 'email and password are required' }
41
+ * 401 { error: 'invalid credentials' } // user not found, password wrong, status disabled
42
+ * 500 { error: 'internal error' }
43
+ *
44
+ * Le handler ne renvoie JAMAIS le password hash, ni d'indication sur quelle
45
+ * partie a échoué (user inexistant vs password faux) — défense contre
46
+ * l'énumération d'emails.
47
+ */
48
+ export function createCredentialsVerifyHandler(config) {
49
+ const defaultRole = config.defaultRole ?? 'user';
50
+ const checkUserAllowed = config.checkUserAllowed ?? defaultStatusCheck;
51
+ const resolveRole = config.resolveRole ?? ((u) => defaultRoleResolver(u, defaultRole));
52
+ const resolveName = config.resolveName ?? defaultNameResolver;
53
+ return async function POST(req) {
54
+ let body;
55
+ try {
56
+ body = await req.json();
57
+ }
58
+ catch {
59
+ return Response.json({ error: 'invalid JSON body' }, { status: 400 });
60
+ }
61
+ const email = body?.email;
62
+ const password = body?.password;
63
+ if (!email || !password) {
64
+ return Response.json({ error: 'email and password are required' }, { status: 400 });
65
+ }
66
+ try {
67
+ const user = await config.findUserByEmail(String(email).toLowerCase().trim());
68
+ if (!user) {
69
+ return Response.json({ error: 'invalid credentials' }, { status: 401 });
70
+ }
71
+ const allowed = await checkUserAllowed(user);
72
+ if (!allowed) {
73
+ return Response.json({ error: 'invalid credentials' }, { status: 401 });
74
+ }
75
+ const valid = await comparePassword(String(password), user.password);
76
+ if (!valid) {
77
+ return Response.json({ error: 'invalid credentials' }, { status: 401 });
78
+ }
79
+ const role = await resolveRole(user);
80
+ const name = resolveName(user);
81
+ return Response.json({
82
+ ok: true,
83
+ user: {
84
+ id: user.id ?? user._id,
85
+ email: user.email,
86
+ name,
87
+ role,
88
+ },
89
+ });
90
+ }
91
+ catch (e) {
92
+ console.error('[auth/verify] internal error:', e?.message || e);
93
+ return Response.json({ error: 'internal error' }, { status: 500 });
94
+ }
95
+ };
96
+ }
@@ -0,0 +1,55 @@
1
+ export interface RemoteCredentialsProviderConfig {
2
+ /** URL of the verify endpoint exposed by the server (Octonet).
3
+ * Absolute or relative. Le serveur doit avoir monté
4
+ * createCredentialsVerifyHandler sur ce path (mosta-net le fait). */
5
+ verifyEndpoint: string;
6
+ /** API key identifiant le caller (REQUIS). Pour Octocloud c'est la portal
7
+ * apikey ; pour une app C#/Java/…, c'est leur propre apikey. Octonet
8
+ * est M2M : pas d'apikey = pas d'accès. */
9
+ apiKey: string;
10
+ /** Optional fetch implementation override (testing, custom transport). */
11
+ fetch?: typeof fetch;
12
+ /** Provider id (default: 'credentials'). */
13
+ id?: string;
14
+ /** Provider display name (default: 'Email + Password'). */
15
+ name?: string;
16
+ /** Optional hook called on successful auth — utile pour syncer un mirror
17
+ * local (ex: une table users local côté caller, en miroir d'Octonet). */
18
+ onAuthorized?: (user: {
19
+ id: string;
20
+ email: string;
21
+ name: string;
22
+ role: string;
23
+ }) => void | Promise<void>;
24
+ }
25
+ /**
26
+ * Build a NextAuth Credentials provider config that delegates verification
27
+ * to a remote HTTP endpoint instead of doing bcrypt.compare locally.
28
+ *
29
+ * Returned shape is identical to createCredentialsProvider — directly
30
+ * passable to `NextAuth({ providers: [provider], ... })`.
31
+ *
32
+ * Le password est POST en HTTPS, le hash bcrypt ne quitte jamais Octonet,
33
+ * et seules les infos sanitisées {id, email, name, role} reviennent.
34
+ */
35
+ export declare function createRemoteCredentialsProvider(config: RemoteCredentialsProviderConfig): {
36
+ id: string;
37
+ name: string;
38
+ type: "credentials";
39
+ credentials: {
40
+ email: {
41
+ label: string;
42
+ type: string;
43
+ };
44
+ password: {
45
+ label: string;
46
+ type: string;
47
+ };
48
+ };
49
+ authorize(credentials: any): Promise<{
50
+ id: any;
51
+ email: any;
52
+ name: any;
53
+ role: any;
54
+ } | null>;
55
+ };
@@ -0,0 +1,115 @@
1
+ // @mostajs/auth — Remote credentials provider for NextAuth (gateway mode)
2
+ //
3
+ // Frère client de createCredentialsVerifyHandler. Là où celui-ci tourne
4
+ // CÔTÉ SERVEUR (Octonet, là où vit la DB) et fait bcrypt.compare local,
5
+ // celui-ci tourne CÔTÉ NextAuth (Octocloud, app C#, app Java, …) et
6
+ // délègue la vérification à un endpoint HTTP au lieu de tenter le
7
+ // compare en local.
8
+ //
9
+ // Pourquoi : en mode gateway (data-plug → MOSTA_DATA=net), le caller n'a
10
+ // pas accès au password hash (le sanitizer middleware d'Octonet le strippe
11
+ // à juste titre). On ne peut pas — et on ne DOIT pas — faire le bcrypt.compare
12
+ // côté caller. Le hash reste sur Octonet.
13
+ //
14
+ // L'`apiKey` est REQUIS : Octonet est M2M et toute requête s'identifie
15
+ // par une apikey. Le caller (Octocloud, ou autre app cliente du portail)
16
+ // envoie SA propre apikey — qui scope les users qu'il peut verify
17
+ // (multi-tenant β via account-scope-middleware).
18
+ //
19
+ // Drop-in replacement de createCredentialsProvider : même shape de retour,
20
+ // même UX NextAuth — seul le `findUserByEmail` callback est remplacé par
21
+ // un appel HTTP à `verifyEndpoint`.
22
+ //
23
+ // Usage typique (Octocloud) :
24
+ // NextAuth({
25
+ // providers: [
26
+ // createRemoteCredentialsProvider({
27
+ // verifyEndpoint: 'https://octonet.amia.fr/api/auth/verify',
28
+ // apiKey: process.env.OCTONET_PORTAL_API_KEY!,
29
+ // }),
30
+ // ],
31
+ // })
32
+ //
33
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
34
+ /**
35
+ * Build a NextAuth Credentials provider config that delegates verification
36
+ * to a remote HTTP endpoint instead of doing bcrypt.compare locally.
37
+ *
38
+ * Returned shape is identical to createCredentialsProvider — directly
39
+ * passable to `NextAuth({ providers: [provider], ... })`.
40
+ *
41
+ * Le password est POST en HTTPS, le hash bcrypt ne quitte jamais Octonet,
42
+ * et seules les infos sanitisées {id, email, name, role} reviennent.
43
+ */
44
+ export function createRemoteCredentialsProvider(config) {
45
+ if (!config.apiKey) {
46
+ throw new Error('@mostajs/auth/createRemoteCredentialsProvider: `apiKey` is required. ' +
47
+ 'Octonet is M2M — every caller (Octocloud, NetClient app, etc.) must identify with its own apikey.');
48
+ }
49
+ if (!config.verifyEndpoint) {
50
+ throw new Error('@mostajs/auth/createRemoteCredentialsProvider: `verifyEndpoint` is required.');
51
+ }
52
+ const fetchImpl = config.fetch ?? fetch;
53
+ return {
54
+ id: config.id ?? 'credentials',
55
+ name: config.name ?? 'Email + Password',
56
+ type: 'credentials',
57
+ credentials: {
58
+ email: { label: 'Email', type: 'email' },
59
+ password: { label: 'Password', type: 'password' },
60
+ },
61
+ async authorize(credentials) {
62
+ if (!credentials?.email || !credentials?.password)
63
+ return null;
64
+ const headers = {
65
+ 'Content-Type': 'application/json',
66
+ 'X-API-Key': config.apiKey,
67
+ };
68
+ let resp;
69
+ try {
70
+ resp = await fetchImpl(config.verifyEndpoint, {
71
+ method: 'POST',
72
+ headers,
73
+ body: JSON.stringify({
74
+ email: String(credentials.email).toLowerCase().trim(),
75
+ password: String(credentials.password),
76
+ }),
77
+ });
78
+ }
79
+ catch (e) {
80
+ console.error('[remote-credentials] network error:', e?.message || e);
81
+ return null;
82
+ }
83
+ if (!resp.ok) {
84
+ // 401 = invalid credentials, 400 = bad request, 500 = server error.
85
+ // NextAuth attend null pour reject — on ne distingue pas (defense
86
+ // contre l'énumération d'emails côté UI).
87
+ return null;
88
+ }
89
+ let payload;
90
+ try {
91
+ payload = await resp.json();
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ if (!payload?.ok || !payload?.user)
97
+ return null;
98
+ const user = payload.user;
99
+ if (config.onAuthorized) {
100
+ try {
101
+ await config.onAuthorized(user);
102
+ }
103
+ catch (e) {
104
+ console.warn('[remote-credentials] onAuthorized hook failed:', e?.message || e);
105
+ }
106
+ }
107
+ return {
108
+ id: user.id,
109
+ email: user.email,
110
+ name: user.name,
111
+ role: user.role,
112
+ };
113
+ },
114
+ };
115
+ }
package/dist/server.d.ts CHANGED
@@ -20,5 +20,11 @@ export { createPasswordResetHandlers, generateResetToken } from './lib/password-
20
20
  export type { PasswordResetConfig } from './lib/password-reset';
21
21
  export { createApiKeyProvider } from './lib/apikey-provider';
22
22
  export type { ApiKeyProviderConfig } from './lib/apikey-provider';
23
+ export { createCredentialsProvider } from './lib/credentials-provider';
24
+ export type { CredentialsProviderConfig } from './lib/credentials-provider';
25
+ export { createCredentialsVerifyHandler } from './lib/credentials-verify';
26
+ export type { CredentialsVerifyConfig } from './lib/credentials-verify';
27
+ export { createRemoteCredentialsProvider } from './lib/remote-credentials-provider';
28
+ export type { RemoteCredentialsProviderConfig } from './lib/remote-credentials-provider';
23
29
  export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment';
24
30
  export type { SessionEnrichmentConfig } from './lib/session-enrichment';
package/dist/server.js CHANGED
@@ -27,5 +27,8 @@ export { createVerificationHandlers, generateVerifyToken } from './lib/email-ver
27
27
  export { createPasswordResetHandlers, generateResetToken } from './lib/password-reset.js';
28
28
  // API key provider
29
29
  export { createApiKeyProvider } from './lib/apikey-provider.js';
30
+ export { createCredentialsProvider } from './lib/credentials-provider.js';
31
+ export { createCredentialsVerifyHandler } from './lib/credentials-verify.js';
32
+ export { createRemoteCredentialsProvider } from './lib/remote-credentials-provider.js';
30
33
  // Session enrichment
31
34
  export { enrichTokenWithPlan, enrichSessionWithPlan } from './lib/session-enrichment.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/auth",
3
- "version": "2.3.3",
3
+ "version": "2.5.0",
4
4
  "description": "Authentication — NextAuth, password hashing, session management",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
@@ -83,6 +83,11 @@
83
83
  "import": "./dist/lib/apikey-provider.js",
84
84
  "default": "./dist/lib/apikey-provider.js"
85
85
  },
86
+ "./lib/credentials-provider": {
87
+ "types": "./dist/lib/credentials-provider.d.ts",
88
+ "import": "./dist/lib/credentials-provider.js",
89
+ "default": "./dist/lib/credentials-provider.js"
90
+ },
86
91
  "./lib/check-request": {
87
92
  "types": "./dist/lib/check-request.d.ts",
88
93
  "import": "./dist/lib/check-request.js",
@@ -120,7 +125,7 @@
120
125
  },
121
126
  "scripts": {
122
127
  "build": "tsc && npm run fix-esm",
123
- "fix-esm": "find dist -name '*.js' -exec sed -i -E \"s|from '(\\\\.{1,2}/[^']+)'(;?)|from '\\\\1.js'\\\\2|g; s|from \\\"(\\\\.{1,2}/[^\\\"]+)\\\"(;?)|from \\\"\\\\1.js\\\"\\\\2|g\" {} \\; && find dist -name '*.js' -exec sed -i -E \"s|\\\\.js\\\\.js|.js|g\" {} \\;",
128
+ "fix-esm": "find dist -name '*.js' -exec sed -i -E \"s|from '(\\\\.{1,2}/[^']+)'(;?)|from '\\\\1.js'\\\\2|g; s|from \\\"(\\\\.{1,2}/[^\\\"]+)\\\"(;?)|from \\\"\\\\1.js\\\"\\\\2|g\" {} \\; && find dist -name '*.js' -exec sed -i -E \"s|\\\\.js\\\\.js|.js|g; s|\\\\.json\\\\.js|.json|g; s|\\\\.css\\\\.js|.css|g\" {} \\;",
124
129
  "prepublishOnly": "npm run build"
125
130
  },
126
131
  "dependencies": {