@rimelight/auth 0.0.7 → 0.0.9
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/dist/adapters/auth0.d.mts +7 -0
- package/dist/adapters/auth0.mjs +56 -1
- package/dist/adapters/cf-access.d.mts +4 -0
- package/dist/adapters/cf-access.mjs +19 -1
- package/dist/client/index.d.mts +32 -0
- package/dist/client/index.mjs +62 -0
- package/dist/index.d.mts +11 -4
- package/dist/index.mjs +23 -3
- package/dist/middleware/index.d.mts +20 -0
- package/dist/middleware/index.mjs +174 -0
- package/dist/types.d.mts +82 -0
- package/dist/vite.d.mts +33 -0
- package/dist/vite.mjs +66 -0
- package/package.json +19 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rimelight Entertainment
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -111,5 +111,12 @@ export declare function mapAuth0PayloadToSession(payload: Record<string, any>, o
|
|
|
111
111
|
permissionsClaim: string;
|
|
112
112
|
defaultRole: string;
|
|
113
113
|
}): UserSessionContext;
|
|
114
|
+
export declare function getAuth0ConfigFromEnv(overrides?: Partial<Auth0AdapterOptions>): Auth0AdapterOptions;
|
|
115
|
+
/**
|
|
116
|
+
* Creates an Auth0 auth adapter automatically configured from environment variables.
|
|
117
|
+
*/
|
|
118
|
+
export declare function createAuth0FromEnv(overrides?: Partial<Auth0AdapterOptions> & {
|
|
119
|
+
devFallback?: boolean;
|
|
120
|
+
}): AuthAdapter;
|
|
114
121
|
//#endregion
|
|
115
122
|
export { decodeJwt };
|
package/dist/adapters/auth0.mjs
CHANGED
|
@@ -252,5 +252,60 @@ function mapAuth0PayloadToSession(payload, options) {
|
|
|
252
252
|
metadata: payload
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
|
+
function getEnv(key, fallback = "") {
|
|
256
|
+
if (typeof process !== "undefined" && process.env?.[key]) return process.env[key];
|
|
257
|
+
if (typeof import.meta !== "undefined" && import.meta.env?.[key]) return import.meta.env[key];
|
|
258
|
+
return fallback;
|
|
259
|
+
}
|
|
260
|
+
function getAuth0ConfigFromEnv(overrides = {}) {
|
|
261
|
+
return {
|
|
262
|
+
domain: overrides.domain || getEnv("AUTH0_DOMAIN", "rimelight.eu.auth0.com"),
|
|
263
|
+
audience: overrides.audience || (getEnv("AUTH0_USE_CUSTOM_AUDIENCE") === "true" ? getEnv("AUTH0_AUDIENCE") || void 0 : void 0),
|
|
264
|
+
clientId: overrides.clientId || getEnv("AUTH0_CLIENT_ID", ""),
|
|
265
|
+
clientSecret: overrides.clientSecret || getEnv("AUTH0_CLIENT_SECRET", ""),
|
|
266
|
+
rolesClaim: overrides.rolesClaim || getEnv("AUTH0_ROLES_CLAIM", "https://rimelight.com/roles"),
|
|
267
|
+
userTypeClaim: overrides.userTypeClaim || getEnv("AUTH0_USER_TYPE_CLAIM", "https://rimelight.com/user_type"),
|
|
268
|
+
permissionsClaim: overrides.permissionsClaim || "permissions",
|
|
269
|
+
sessionCookieName: overrides.sessionCookieName || "rimelight_session",
|
|
270
|
+
sessionSecret: overrides.sessionSecret || getEnv("AUTH0_SECRET", "rimelight_default_development_secret_key_32bytes_minimum_length"),
|
|
271
|
+
loginUrl: overrides.loginUrl || "/api/auth/login",
|
|
272
|
+
defaultRole: overrides.defaultRole || "user",
|
|
273
|
+
...overrides
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Creates an Auth0 auth adapter automatically configured from environment variables.
|
|
278
|
+
*/
|
|
279
|
+
function createAuth0FromEnv(overrides = {}) {
|
|
280
|
+
const config = getAuth0ConfigFromEnv(overrides);
|
|
281
|
+
const baseAdapter = auth0Auth(config);
|
|
282
|
+
if (!overrides.devFallback) return baseAdapter;
|
|
283
|
+
return {
|
|
284
|
+
name: "auth0",
|
|
285
|
+
options: config,
|
|
286
|
+
async getSession(req) {
|
|
287
|
+
const session = await baseAdapter.getSession(req);
|
|
288
|
+
if (session) return session;
|
|
289
|
+
if ((typeof process !== "undefined" && process.env?.["NODE_ENV"] === "development" || typeof import.meta !== "undefined" && import.meta.env?.DEV) && (!config.clientId || config.clientId === "" || config.clientId === "default_client_id")) return {
|
|
290
|
+
userId: "dev-admin-user",
|
|
291
|
+
email: "admin@rimelight.com",
|
|
292
|
+
name: "Admin (Dev)",
|
|
293
|
+
avatar: "https://cdn.rimelight.com/Images/default_avatar.png",
|
|
294
|
+
roles: [
|
|
295
|
+
"admin",
|
|
296
|
+
"owner",
|
|
297
|
+
"editor"
|
|
298
|
+
],
|
|
299
|
+
permissions: ["*"],
|
|
300
|
+
userType: "employee",
|
|
301
|
+
metadata: { isDev: true }
|
|
302
|
+
};
|
|
303
|
+
return null;
|
|
304
|
+
},
|
|
305
|
+
handleUnauthorized(req) {
|
|
306
|
+
return baseAdapter.handleUnauthorized?.(req) ?? new Response("Unauthorized", { status: 401 });
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
255
310
|
//#endregion
|
|
256
|
-
export { auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken };
|
|
311
|
+
export { auth0Auth, createAuth0AuthorizeUrl, createAuth0FromEnv, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, getAuth0ConfigFromEnv, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken };
|
|
@@ -9,4 +9,8 @@ export interface CfAccessAdapterOptions {
|
|
|
9
9
|
loginUrl?: string | undefined;
|
|
10
10
|
}
|
|
11
11
|
export declare function cfAccessAuth(options?: CfAccessAdapterOptions): AuthAdapter;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a Cloudflare Access auth adapter automatically configured from environment variables.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createCfAccessFromEnv(overrides?: Partial<CfAccessAdapterOptions>): AuthAdapter;
|
|
12
16
|
//#endregion
|
|
@@ -62,5 +62,23 @@ function cfAccessAuth(options = {}) {
|
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
function getEnv(key, fallback = "") {
|
|
66
|
+
if (typeof process !== "undefined" && process.env?.[key]) return process.env[key];
|
|
67
|
+
if (typeof import.meta !== "undefined" && import.meta.env?.[key]) return import.meta.env[key];
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates a Cloudflare Access auth adapter automatically configured from environment variables.
|
|
72
|
+
*/
|
|
73
|
+
function createCfAccessFromEnv(overrides = {}) {
|
|
74
|
+
return cfAccessAuth({
|
|
75
|
+
teamDomain: overrides.teamDomain || getEnv("CF_ACCESS_TEAM_DOMAIN"),
|
|
76
|
+
aud: overrides.aud || getEnv("CF_ACCESS_AUD"),
|
|
77
|
+
verifyJwt: overrides.verifyJwt ?? (getEnv("CF_ACCESS_VERIFY_JWT") === "true" || typeof import.meta !== "undefined" && import.meta.env?.["CF_ACCESS_VERIFY_JWT"] === "true"),
|
|
78
|
+
adminEmails: overrides.adminEmails || (getEnv("CF_ACCESS_ADMIN_EMAILS") ? getEnv("CF_ACCESS_ADMIN_EMAILS").split(",").map((e) => e.trim()) : []),
|
|
79
|
+
defaultRole: overrides.defaultRole || "admin",
|
|
80
|
+
...overrides
|
|
81
|
+
});
|
|
82
|
+
}
|
|
65
83
|
//#endregion
|
|
66
|
-
export { cfAccessAuth };
|
|
84
|
+
export { cfAccessAuth, createCfAccessFromEnv };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { UserSessionContext } from "../types.mjs";
|
|
2
|
+
//#region src/client/index.d.ts
|
|
3
|
+
export interface AuthClientSession<TUser = UserSessionContext> {
|
|
4
|
+
user: TUser | null;
|
|
5
|
+
session: {
|
|
6
|
+
id: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
roles?: string[];
|
|
9
|
+
userType?: string;
|
|
10
|
+
} | null;
|
|
11
|
+
}
|
|
12
|
+
export interface AuthClientOptions {
|
|
13
|
+
sessionEndpoint?: string;
|
|
14
|
+
meEndpoint?: string;
|
|
15
|
+
loginEndpoint?: string;
|
|
16
|
+
logoutEndpoint?: string;
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
}
|
|
19
|
+
export interface AuthClient<TUser = UserSessionContext> {
|
|
20
|
+
getSession(): Promise<AuthClientSession<TUser>>;
|
|
21
|
+
getMe(): Promise<TUser | null>;
|
|
22
|
+
login(returnTo?: string, extraParams?: Record<string, string>): void;
|
|
23
|
+
signup(returnTo?: string, extraParams?: Record<string, string>): void;
|
|
24
|
+
signOut(returnTo?: string): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Universal browser-side Auth Client factory.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createAuthClient<TUser = UserSessionContext>(options?: AuthClientOptions): AuthClient<TUser>;
|
|
30
|
+
export declare const authClient: AuthClient<UserSessionContext>;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { authClient as default };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/client/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Universal browser-side Auth Client factory.
|
|
4
|
+
*/
|
|
5
|
+
function createAuthClient(options = {}) {
|
|
6
|
+
const { sessionEndpoint = "/api/auth/session", meEndpoint = "/api/auth/me", loginEndpoint = "/api/auth/login", logoutEndpoint = "/api/auth/logout", fetch: customFetch = typeof fetch !== "undefined" ? fetch : void 0 } = options;
|
|
7
|
+
return {
|
|
8
|
+
async getSession() {
|
|
9
|
+
try {
|
|
10
|
+
if (!customFetch) return {
|
|
11
|
+
user: null,
|
|
12
|
+
session: null
|
|
13
|
+
};
|
|
14
|
+
const res = await customFetch(sessionEndpoint);
|
|
15
|
+
if (!res.ok) return {
|
|
16
|
+
user: null,
|
|
17
|
+
session: null
|
|
18
|
+
};
|
|
19
|
+
return await res.json();
|
|
20
|
+
} catch {
|
|
21
|
+
return {
|
|
22
|
+
user: null,
|
|
23
|
+
session: null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async getMe() {
|
|
28
|
+
try {
|
|
29
|
+
if (!customFetch) return null;
|
|
30
|
+
const res = await customFetch(meEndpoint);
|
|
31
|
+
if (!res.ok) return null;
|
|
32
|
+
return (await res.json())?.user ?? null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
login(returnTo = "/", extraParams = {}) {
|
|
38
|
+
if (typeof window === "undefined") return;
|
|
39
|
+
const url = new URL(loginEndpoint, window.location.origin);
|
|
40
|
+
url.searchParams.set("returnTo", returnTo);
|
|
41
|
+
for (const [k, v] of Object.entries(extraParams)) url.searchParams.set(k, v);
|
|
42
|
+
window.location.href = url.toString();
|
|
43
|
+
},
|
|
44
|
+
signup(returnTo = "/", extraParams = {}) {
|
|
45
|
+
if (typeof window === "undefined") return;
|
|
46
|
+
const url = new URL(loginEndpoint, window.location.origin);
|
|
47
|
+
url.searchParams.set("screen_hint", "signup");
|
|
48
|
+
url.searchParams.set("returnTo", returnTo);
|
|
49
|
+
for (const [k, v] of Object.entries(extraParams)) url.searchParams.set(k, v);
|
|
50
|
+
window.location.href = url.toString();
|
|
51
|
+
},
|
|
52
|
+
signOut(returnTo = "/") {
|
|
53
|
+
if (typeof window === "undefined") return;
|
|
54
|
+
const url = new URL(logoutEndpoint, window.location.origin);
|
|
55
|
+
url.searchParams.set("returnTo", returnTo);
|
|
56
|
+
window.location.href = url.toString();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const authClient = createAuthClient();
|
|
61
|
+
//#endregion
|
|
62
|
+
export { authClient, authClient as default, createAuthClient };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { AuthAdapter, UserSessionContext, UserType } from "./types.mjs";
|
|
2
|
-
import { Auth0AdapterOptions, Auth0AuthorizeUrlOptions, Auth0CodeExchangeOptions, Auth0PasswordLoginOptions, Auth0RefreshTokenOptions, Auth0TokenResponse, auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken } from "./adapters/auth0.mjs";
|
|
3
|
-
import { CfAccessAdapterOptions, cfAccessAuth } from "./adapters/cf-access.mjs";
|
|
1
|
+
import { AuthAdapter, AuthMiddlewareOptions, RoleGuard, UserSessionContext, UserType } from "./types.mjs";
|
|
2
|
+
import { Auth0AdapterOptions, Auth0AuthorizeUrlOptions, Auth0CodeExchangeOptions, Auth0PasswordLoginOptions, Auth0RefreshTokenOptions, Auth0TokenResponse, auth0Auth, createAuth0AuthorizeUrl, createAuth0FromEnv, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, getAuth0ConfigFromEnv, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken } from "./adapters/auth0.mjs";
|
|
3
|
+
import { CfAccessAdapterOptions, cfAccessAuth, createCfAccessFromEnv } from "./adapters/cf-access.mjs";
|
|
4
4
|
import { MockAuthOptions, mockAuth } from "./adapters/mock.mjs";
|
|
5
|
+
import { AuthClient, AuthClientOptions, AuthClientSession, authClient, createAuthClient } from "./client/index.mjs";
|
|
5
6
|
import { AccessControl, AccessRole, StatementMap, adminAc, createAccessControl, createPermissions, defaultStatements, evaluateAccess, hasPermission, hasRole, memberAc, ownerAc } from "./permissions/index.mjs";
|
|
6
7
|
import { RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, createRestrictedUsernameSet, normalizeUsername } from "./plugins/reserved-usernames/index.mjs";
|
|
7
8
|
import { CONSTRUCTION_GUEST_COOKIE, GuestEnv, isConstructionGuest, signInConstructionGuest } from "./plugins/construction-guest/index.mjs";
|
|
8
|
-
|
|
9
|
+
import { authMiddleware } from "./middleware/index.mjs";
|
|
10
|
+
import { RimelightAuthPlugin, RimelightAuthPluginOptions } from "./vite.mjs";
|
|
11
|
+
//#region src/index.d.ts
|
|
12
|
+
export declare const auth: AuthAdapter;
|
|
13
|
+
export declare const authAdapter: AuthAdapter;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { AccessControl, AccessRole, Auth0AdapterOptions, Auth0AuthorizeUrlOptions, Auth0CodeExchangeOptions, Auth0PasswordLoginOptions, Auth0RefreshTokenOptions, Auth0TokenResponse, AuthAdapter, AuthClient, AuthClientOptions, AuthClientSession, AuthMiddlewareOptions, CONSTRUCTION_GUEST_COOKIE, CfAccessAdapterOptions, GuestEnv, MockAuthOptions, RESTRICTED_SET, RimelightAuthPlugin, RimelightAuthPluginOptions, RoleGuard, STANDARD_RESTRICTED_GROUPS, StatementMap, UserSessionContext, UserType, adminAc, auth as default, auth0Auth, authClient, authMiddleware, cfAccessAuth, createAccessControl, createAuth0AuthorizeUrl, createAuth0FromEnv, createAuth0LogoutUrl, createAuthClient, createCfAccessFromEnv, createPermissions, createRestrictedUsernameSet, createSessionToken, decodeJwt, defaultStatements, evaluateAccess, exchangeAuth0Code, getAuth0ConfigFromEnv, hasPermission, hasRole, isConstructionGuest, loginWithAuth0Password, mapAuth0PayloadToSession, memberAc, mockAuth, normalizeUsername, ownerAc, refreshAuth0Token, signInConstructionGuest, verifyAuth0Jwt, verifySessionToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
import { auth0Auth, createAuth0AuthorizeUrl, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken } from "./adapters/auth0.mjs";
|
|
2
|
-
import { cfAccessAuth } from "./adapters/cf-access.mjs";
|
|
1
|
+
import { auth0Auth, createAuth0AuthorizeUrl, createAuth0FromEnv, createAuth0LogoutUrl, createSessionToken, decodeJwt, exchangeAuth0Code, getAuth0ConfigFromEnv, loginWithAuth0Password, mapAuth0PayloadToSession, refreshAuth0Token, verifyAuth0Jwt, verifySessionToken } from "./adapters/auth0.mjs";
|
|
2
|
+
import { cfAccessAuth, createCfAccessFromEnv } from "./adapters/cf-access.mjs";
|
|
3
3
|
import { mockAuth } from "./adapters/mock.mjs";
|
|
4
|
+
import { authClient, createAuthClient } from "./client/index.mjs";
|
|
4
5
|
import "./types.mjs";
|
|
5
6
|
import { AccessControl, adminAc, createAccessControl, createPermissions, defaultStatements, evaluateAccess, hasPermission, hasRole, memberAc, ownerAc } from "./permissions/index.mjs";
|
|
6
7
|
import { RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, createRestrictedUsernameSet, normalizeUsername } from "./plugins/reserved-usernames/index.mjs";
|
|
7
8
|
import { CONSTRUCTION_GUEST_COOKIE, isConstructionGuest, signInConstructionGuest } from "./plugins/construction-guest/index.mjs";
|
|
8
|
-
|
|
9
|
+
import { authMiddleware } from "./middleware/index.mjs";
|
|
10
|
+
import "./vite.mjs";
|
|
11
|
+
//#region src/index.ts
|
|
12
|
+
let _defaultAuth = null;
|
|
13
|
+
function getDefaultAuth() {
|
|
14
|
+
if (!_defaultAuth) _defaultAuth = typeof process !== "undefined" && Boolean(process.env?.["AUTH0_DOMAIN"]) || typeof import.meta !== "undefined" && Boolean(import.meta.env?.["AUTH0_DOMAIN"]) ? createAuth0FromEnv({ devFallback: true }) : createCfAccessFromEnv();
|
|
15
|
+
return _defaultAuth;
|
|
16
|
+
}
|
|
17
|
+
const auth = {
|
|
18
|
+
name: "auto",
|
|
19
|
+
async getSession(req) {
|
|
20
|
+
return getDefaultAuth().getSession(req);
|
|
21
|
+
},
|
|
22
|
+
handleUnauthorized(req) {
|
|
23
|
+
return getDefaultAuth().handleUnauthorized?.(req) ?? new Response("Unauthorized", { status: 401 });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const authAdapter = auth;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { AccessControl, CONSTRUCTION_GUEST_COOKIE, RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, adminAc, auth, auth as default, auth0Auth, authAdapter, authClient, authMiddleware, cfAccessAuth, createAccessControl, createAuth0AuthorizeUrl, createAuth0FromEnv, createAuth0LogoutUrl, createAuthClient, createCfAccessFromEnv, createPermissions, createRestrictedUsernameSet, createSessionToken, decodeJwt, defaultStatements, evaluateAccess, exchangeAuth0Code, getAuth0ConfigFromEnv, hasPermission, hasRole, isConstructionGuest, loginWithAuth0Password, mapAuth0PayloadToSession, memberAc, mockAuth, normalizeUsername, ownerAc, refreshAuth0Token, signInConstructionGuest, verifyAuth0Jwt, verifySessionToken };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AuthMiddlewareOptions } from "../types.mjs";
|
|
2
|
+
//#region src/middleware/index.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Universal Authentication & Authorization middleware for Hono applications.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { authMiddleware } from "@rimelight/auth/middleware"
|
|
8
|
+
* import { auth } from "#auth/auth"
|
|
9
|
+
*
|
|
10
|
+
* app.use(
|
|
11
|
+
* authMiddleware({
|
|
12
|
+
* auth,
|
|
13
|
+
* roleGuards: {
|
|
14
|
+
* "/admin": ["admin", "owner"]
|
|
15
|
+
* }
|
|
16
|
+
* })
|
|
17
|
+
* )
|
|
18
|
+
*/
|
|
19
|
+
export declare const authMiddleware: (options?: AuthMiddlewareOptions) => (c: any, next: () => Promise<any> | any) => Promise<Response | any>;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { isConstructionGuest } from "../plugins/construction-guest/index.mjs";
|
|
2
|
+
//#region src/middleware/index.ts
|
|
3
|
+
const STATIC_ASSET_REGEX = /\.(css|js|map|png|jpg|jpeg|gif|svg|webp|woff|woff2|ttf|ico|json|webmanifest)$/i;
|
|
4
|
+
const DEFAULT_IGNORED_ROUTES = ["/docs/"];
|
|
5
|
+
const DEFAULT_PROTECTED_ROUTES = ["/internal"];
|
|
6
|
+
const DEFAULT_GUEST_ONLY_ROUTES = ["/auth/sign-in", "/auth/sign-up"];
|
|
7
|
+
function matchesPattern(path, pattern) {
|
|
8
|
+
if (typeof pattern === "function") return pattern(path);
|
|
9
|
+
if (typeof pattern === "string") return path.startsWith(pattern) || path.includes(pattern);
|
|
10
|
+
return pattern.test(path);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Universal Authentication & Authorization middleware for Hono applications.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { authMiddleware } from "@rimelight/auth/middleware"
|
|
17
|
+
* import { auth } from "#auth/auth"
|
|
18
|
+
*
|
|
19
|
+
* app.use(
|
|
20
|
+
* authMiddleware({
|
|
21
|
+
* auth,
|
|
22
|
+
* roleGuards: {
|
|
23
|
+
* "/admin": ["admin", "owner"]
|
|
24
|
+
* }
|
|
25
|
+
* })
|
|
26
|
+
* )
|
|
27
|
+
*/
|
|
28
|
+
const authMiddleware = (options = {}) => {
|
|
29
|
+
const { auth, allowConstructionGuest = true, skipStaticAssets = true, ignoredRoutes = DEFAULT_IGNORED_ROUTES, protectedRoutes = DEFAULT_PROTECTED_ROUTES, roleGuards = {}, guestOnlyRoutes = DEFAULT_GUEST_ONLY_ROUTES, authRedirectUrl = "/auth/sign-in", signInUrl = "/auth/sign-in", homeUrl = "/", onUnauthorized, onForbidden } = options;
|
|
30
|
+
return async (c, next) => {
|
|
31
|
+
const rawUrl = c?.req?.url || "http://localhost/";
|
|
32
|
+
const pathname = new URL(rawUrl).pathname;
|
|
33
|
+
const isApi = pathname.startsWith("/api/");
|
|
34
|
+
if (skipStaticAssets) {
|
|
35
|
+
if (pathname.startsWith("/_astro/") || pathname.startsWith("/assets/") || pathname === "/favicon.ico" || pathname.startsWith("/open-graph/") || STATIC_ASSET_REGEX.test(pathname)) return await next();
|
|
36
|
+
}
|
|
37
|
+
if (typeof ignoredRoutes === "function" ? ignoredRoutes(pathname) : ignoredRoutes.some((pattern) => matchesPattern(pathname, pattern))) {
|
|
38
|
+
if (typeof c?.set === "function") {
|
|
39
|
+
c.set("user", null);
|
|
40
|
+
c.set("session", null);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
let sessionCtx = null;
|
|
44
|
+
if (auth) try {
|
|
45
|
+
if (typeof auth === "function") sessionCtx = await auth(c);
|
|
46
|
+
else if (typeof auth.getSession === "function") {
|
|
47
|
+
const req = c.req?.raw || c.req;
|
|
48
|
+
sessionCtx = await auth.getSession(req);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error("[authMiddleware] Failed to resolve session:", err);
|
|
52
|
+
}
|
|
53
|
+
let isGuest = false;
|
|
54
|
+
if (!sessionCtx && allowConstructionGuest) try {
|
|
55
|
+
isGuest = await isConstructionGuest(c);
|
|
56
|
+
} catch {
|
|
57
|
+
isGuest = false;
|
|
58
|
+
}
|
|
59
|
+
const user = sessionCtx ?? (isGuest ? {
|
|
60
|
+
userId: "construction-guest",
|
|
61
|
+
email: "guest",
|
|
62
|
+
name: "Guest",
|
|
63
|
+
roles: ["guest"],
|
|
64
|
+
permissions: [],
|
|
65
|
+
userType: "user",
|
|
66
|
+
metadata: {}
|
|
67
|
+
} : null);
|
|
68
|
+
const session = sessionCtx ? {
|
|
69
|
+
id: sessionCtx.userId,
|
|
70
|
+
userId: sessionCtx.userId,
|
|
71
|
+
roles: sessionCtx.roles,
|
|
72
|
+
userType: sessionCtx.userType
|
|
73
|
+
} : isGuest ? {
|
|
74
|
+
id: "construction-guest",
|
|
75
|
+
userId: "construction-guest",
|
|
76
|
+
roles: ["guest"],
|
|
77
|
+
userType: "user"
|
|
78
|
+
} : null;
|
|
79
|
+
if (typeof c?.set === "function") {
|
|
80
|
+
c.set("user", user);
|
|
81
|
+
c.set("session", session);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const session = typeof c?.get === "function" ? c.get("session") : null;
|
|
85
|
+
const user = typeof c?.get === "function" ? c.get("user") : null;
|
|
86
|
+
if (pathname === "/auth") return c.redirect(authRedirectUrl);
|
|
87
|
+
if (guestOnlyRoutes.some((route) => matchesPattern(pathname, route)) && session) return c.redirect(homeUrl);
|
|
88
|
+
if (pathname.includes("/construction") && session) return c.redirect(homeUrl);
|
|
89
|
+
if ((typeof protectedRoutes === "function" ? protectedRoutes(pathname) : protectedRoutes.some((pattern) => matchesPattern(pathname, pattern))) && !session) {
|
|
90
|
+
if (onUnauthorized) return onUnauthorized(c, {
|
|
91
|
+
isApi,
|
|
92
|
+
path: pathname
|
|
93
|
+
});
|
|
94
|
+
if (isApi) {
|
|
95
|
+
if (typeof c.json === "function") return c.json({
|
|
96
|
+
error: "Unauthorized",
|
|
97
|
+
message: "Authentication required."
|
|
98
|
+
}, 401);
|
|
99
|
+
return new Response(JSON.stringify({
|
|
100
|
+
error: "Unauthorized",
|
|
101
|
+
message: "Authentication required."
|
|
102
|
+
}), {
|
|
103
|
+
status: 401,
|
|
104
|
+
headers: { "Content-Type": "application/json" }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return c.redirect(signInUrl);
|
|
108
|
+
}
|
|
109
|
+
for (const [guardPath, guardConfig] of Object.entries(roleGuards)) if (matchesPattern(pathname, guardPath)) {
|
|
110
|
+
if (!session) {
|
|
111
|
+
if (onUnauthorized) return onUnauthorized(c, {
|
|
112
|
+
isApi,
|
|
113
|
+
path: pathname
|
|
114
|
+
});
|
|
115
|
+
if (isApi) {
|
|
116
|
+
if (typeof c.json === "function") return c.json({
|
|
117
|
+
error: "Unauthorized",
|
|
118
|
+
message: "Authentication required."
|
|
119
|
+
}, 401);
|
|
120
|
+
return new Response(JSON.stringify({
|
|
121
|
+
error: "Unauthorized",
|
|
122
|
+
message: "Authentication required."
|
|
123
|
+
}), {
|
|
124
|
+
status: 401,
|
|
125
|
+
headers: { "Content-Type": "application/json" }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return c.redirect(signInUrl);
|
|
129
|
+
}
|
|
130
|
+
let isAllowed = false;
|
|
131
|
+
const userRoles = (user?.roles || []).map((r) => r.toLowerCase());
|
|
132
|
+
const userType = user?.userType;
|
|
133
|
+
if (Array.isArray(guardConfig)) {
|
|
134
|
+
const allowedRoles = guardConfig.map((r) => r.toLowerCase());
|
|
135
|
+
isAllowed = userRoles.includes("*") || allowedRoles.some((r) => userRoles.includes(r));
|
|
136
|
+
} else if (typeof guardConfig === "object" && guardConfig !== null) {
|
|
137
|
+
const { roles, userTypes, check } = guardConfig;
|
|
138
|
+
if (check) isAllowed = await check(user, c);
|
|
139
|
+
else {
|
|
140
|
+
const roleMatch = roles ? userRoles.includes("*") || roles.some((r) => userRoles.includes(r.toLowerCase())) : true;
|
|
141
|
+
const typeMatch = userTypes ? userType ? userTypes.includes(userType) : false : true;
|
|
142
|
+
isAllowed = roleMatch && typeMatch;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!isAllowed) {
|
|
146
|
+
if (onForbidden) {
|
|
147
|
+
const requiredRoles = Array.isArray(guardConfig) ? guardConfig : guardConfig.roles;
|
|
148
|
+
return onForbidden(c, {
|
|
149
|
+
isApi,
|
|
150
|
+
path: pathname,
|
|
151
|
+
requiredRoles
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (isApi) {
|
|
155
|
+
if (typeof c.json === "function") return c.json({
|
|
156
|
+
error: "Forbidden",
|
|
157
|
+
message: "Forbidden: insufficient permissions."
|
|
158
|
+
}, 403);
|
|
159
|
+
return new Response(JSON.stringify({
|
|
160
|
+
error: "Forbidden",
|
|
161
|
+
message: "Forbidden: insufficient permissions."
|
|
162
|
+
}), {
|
|
163
|
+
status: 403,
|
|
164
|
+
headers: { "Content-Type": "application/json" }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return c.redirect(homeUrl);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return await next();
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
export { authMiddleware };
|
package/dist/types.d.mts
CHANGED
|
@@ -54,4 +54,86 @@ export interface AuthAdapter {
|
|
|
54
54
|
*/
|
|
55
55
|
handleUnauthorized?: ((request: Request) => Response | Promise<Response>) | undefined;
|
|
56
56
|
}
|
|
57
|
+
export interface RoleGuard {
|
|
58
|
+
roles?: string[];
|
|
59
|
+
userTypes?: UserType[];
|
|
60
|
+
check?: (user: UserSessionContext | null, c: any) => boolean | Promise<boolean>;
|
|
61
|
+
}
|
|
62
|
+
export interface AuthMiddlewareOptions {
|
|
63
|
+
/**
|
|
64
|
+
* The auth adapter instance or custom session resolver function.
|
|
65
|
+
*/
|
|
66
|
+
auth?: AuthAdapter | {
|
|
67
|
+
getSession(req: Request): Promise<UserSessionContext | null>;
|
|
68
|
+
} | ((c: any) => Promise<UserSessionContext | null>);
|
|
69
|
+
/**
|
|
70
|
+
* Fall back to construction guest session if no user is signed in.
|
|
71
|
+
*
|
|
72
|
+
* @default true
|
|
73
|
+
*/
|
|
74
|
+
allowConstructionGuest?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether to automatically skip static asset paths.
|
|
77
|
+
*
|
|
78
|
+
* @default true
|
|
79
|
+
*/
|
|
80
|
+
skipStaticAssets?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Route patterns to completely bypass session resolution.
|
|
83
|
+
*
|
|
84
|
+
* @default ["/docs/"]
|
|
85
|
+
*/
|
|
86
|
+
ignoredRoutes?: (string | RegExp)[] | ((path: string) => boolean);
|
|
87
|
+
/**
|
|
88
|
+
* Route patterns that require an authenticated session.
|
|
89
|
+
*
|
|
90
|
+
* @default ["/internal"]
|
|
91
|
+
*/
|
|
92
|
+
protectedRoutes?: (string | RegExp)[] | ((path: string) => boolean);
|
|
93
|
+
/**
|
|
94
|
+
* Role / permission requirements for specific route prefixes. E.g.: { "/admin": ["admin",
|
|
95
|
+
* "owner"], "/internal": { roles: ["employee", "staff", "admin", "owner"], userTypes:
|
|
96
|
+
* ["employee"] } }
|
|
97
|
+
*/
|
|
98
|
+
roleGuards?: Record<string, string[] | RoleGuard>;
|
|
99
|
+
/**
|
|
100
|
+
* Routes that should redirect to home/dashboard if user is already logged in.
|
|
101
|
+
*
|
|
102
|
+
* @default ["/auth/sign-in", "/auth/sign-up"]
|
|
103
|
+
*/
|
|
104
|
+
guestOnlyRoutes?: (string | RegExp)[];
|
|
105
|
+
/**
|
|
106
|
+
* URL to redirect to when `/auth` is accessed directly.
|
|
107
|
+
*
|
|
108
|
+
* @default "/auth/sign-in"
|
|
109
|
+
*/
|
|
110
|
+
authRedirectUrl?: string;
|
|
111
|
+
/**
|
|
112
|
+
* URL to redirect unauthenticated users accessing protected pages.
|
|
113
|
+
*
|
|
114
|
+
* @default "/auth/sign-in"
|
|
115
|
+
*/
|
|
116
|
+
signInUrl?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Home URL to redirect logged-in users away from guest routes or forbidden pages.
|
|
119
|
+
*
|
|
120
|
+
* @default "/"
|
|
121
|
+
*/
|
|
122
|
+
homeUrl?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Custom response when unauthorized (not logged in).
|
|
125
|
+
*/
|
|
126
|
+
onUnauthorized?: (c: any, meta: {
|
|
127
|
+
isApi: boolean;
|
|
128
|
+
path: string;
|
|
129
|
+
}) => Response | Promise<Response>;
|
|
130
|
+
/**
|
|
131
|
+
* Custom response when forbidden (insufficient role/permissions).
|
|
132
|
+
*/
|
|
133
|
+
onForbidden?: (c: any, meta: {
|
|
134
|
+
isApi: boolean;
|
|
135
|
+
path: string;
|
|
136
|
+
requiredRoles?: string[];
|
|
137
|
+
}) => Response | Promise<Response>;
|
|
138
|
+
}
|
|
57
139
|
//#endregion
|
package/dist/vite.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/vite.d.ts
|
|
2
|
+
export interface RimelightAuthPluginOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Auth adapter to initialize: "cf-access" (default), "auth0", "mock", or "custom".
|
|
5
|
+
*
|
|
6
|
+
* @default "cf-access"
|
|
7
|
+
*/
|
|
8
|
+
adapter?: "cf-access" | "auth0" | "mock" | "custom" | (string & {});
|
|
9
|
+
/**
|
|
10
|
+
* Path to custom auth module if adapter is set to "custom".
|
|
11
|
+
*/
|
|
12
|
+
customModule?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Additional adapter-specific configuration overrides.
|
|
15
|
+
*/
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
export interface RimelightAuthPlugin {
|
|
19
|
+
name: string;
|
|
20
|
+
resolveId: (id: string) => string | null | undefined;
|
|
21
|
+
load: (id: string) => string | null | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Pure Vite Auth Plugin for Rimelight applications.
|
|
25
|
+
*
|
|
26
|
+
* Exposes virtual modules:
|
|
27
|
+
*
|
|
28
|
+
* - `virtual:rimelight-auth` (alias `#auth/auth`, `#auth`)
|
|
29
|
+
* - `virtual:rimelight-auth-client` (alias `#auth/auth-client`)
|
|
30
|
+
* - `virtual:rimelight-auth-config`
|
|
31
|
+
*/
|
|
32
|
+
export declare function auth(options?: RimelightAuthPluginOptions): RimelightAuthPlugin;
|
|
33
|
+
//#endregion
|
package/dist/vite.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//#region src/vite.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pure Vite Auth Plugin for Rimelight applications.
|
|
4
|
+
*
|
|
5
|
+
* Exposes virtual modules:
|
|
6
|
+
*
|
|
7
|
+
* - `virtual:rimelight-auth` (alias `#auth/auth`, `#auth`)
|
|
8
|
+
* - `virtual:rimelight-auth-client` (alias `#auth/auth-client`)
|
|
9
|
+
* - `virtual:rimelight-auth-config`
|
|
10
|
+
*/
|
|
11
|
+
function auth(options = {}) {
|
|
12
|
+
const adapter = options.adapter || "cf-access";
|
|
13
|
+
const virtualAuthId = "virtual:rimelight-auth";
|
|
14
|
+
const resolvedVirtualAuthId = "\0" + virtualAuthId;
|
|
15
|
+
const virtualClientId = "virtual:rimelight-auth-client";
|
|
16
|
+
const resolvedVirtualClientId = "\0" + virtualClientId;
|
|
17
|
+
const virtualConfigId = "virtual:rimelight-auth-config";
|
|
18
|
+
const resolvedVirtualConfigId = "\0" + virtualConfigId;
|
|
19
|
+
return {
|
|
20
|
+
name: "vite-plugin-rimelight-auth",
|
|
21
|
+
resolveId(id) {
|
|
22
|
+
if (id === virtualAuthId || id === "#auth" || id === "#auth/auth") return resolvedVirtualAuthId;
|
|
23
|
+
if (id === virtualClientId || id === "#auth/auth-client") return resolvedVirtualClientId;
|
|
24
|
+
if (id === virtualConfigId) return resolvedVirtualConfigId;
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
27
|
+
load(id) {
|
|
28
|
+
if (id === resolvedVirtualConfigId) return `export const config = ${JSON.stringify(options)};`;
|
|
29
|
+
if (id === resolvedVirtualClientId) return `
|
|
30
|
+
import { createAuthClient, authClient } from "@rimelight/auth/client";
|
|
31
|
+
export { createAuthClient, authClient };
|
|
32
|
+
export default authClient;
|
|
33
|
+
`;
|
|
34
|
+
if (id === resolvedVirtualAuthId) {
|
|
35
|
+
if (adapter === "custom" && options.customModule) return `
|
|
36
|
+
export * from "${options.customModule}";
|
|
37
|
+
export { default } from "${options.customModule}";
|
|
38
|
+
`;
|
|
39
|
+
if (adapter === "auth0") return `
|
|
40
|
+
import { createAuth0FromEnv } from "@rimelight/auth/auth0";
|
|
41
|
+
export const auth = createAuth0FromEnv(${JSON.stringify({
|
|
42
|
+
devFallback: true,
|
|
43
|
+
...options
|
|
44
|
+
})});
|
|
45
|
+
export const authAdapter = auth;
|
|
46
|
+
export default auth;
|
|
47
|
+
`;
|
|
48
|
+
if (adapter === "mock") return `
|
|
49
|
+
import { mockAuth } from "@rimelight/auth/mock";
|
|
50
|
+
export const auth = mockAuth(${JSON.stringify(options)});
|
|
51
|
+
export const authAdapter = auth;
|
|
52
|
+
export default auth;
|
|
53
|
+
`;
|
|
54
|
+
return `
|
|
55
|
+
import { createCfAccessFromEnv } from "@rimelight/auth/cf-access";
|
|
56
|
+
export const auth = createCfAccessFromEnv(${JSON.stringify(options)});
|
|
57
|
+
export const authAdapter = auth;
|
|
58
|
+
export default auth;
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { auth };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's Universal Authentication & Identity Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -51,22 +51,29 @@
|
|
|
51
51
|
"./plugins/construction-guest": {
|
|
52
52
|
"types": "./dist/plugins/construction-guest/index.d.mts",
|
|
53
53
|
"import": "./dist/plugins/construction-guest/index.mjs"
|
|
54
|
+
},
|
|
55
|
+
"./middleware": {
|
|
56
|
+
"types": "./dist/middleware/index.d.mts",
|
|
57
|
+
"import": "./dist/middleware/index.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./client": {
|
|
60
|
+
"types": "./dist/client/index.d.mts",
|
|
61
|
+
"import": "./dist/client/index.mjs"
|
|
62
|
+
},
|
|
63
|
+
"./vite": {
|
|
64
|
+
"types": "./dist/vite.d.mts",
|
|
65
|
+
"import": "./dist/vite.mjs"
|
|
54
66
|
}
|
|
55
67
|
},
|
|
56
68
|
"publishConfig": {
|
|
57
69
|
"access": "public"
|
|
58
70
|
},
|
|
59
|
-
"scripts": {
|
|
60
|
-
"build": "vp pack",
|
|
61
|
-
"prepack": "pnpm run build",
|
|
62
|
-
"check": "vp check --fix"
|
|
63
|
-
},
|
|
64
71
|
"dependencies": {
|
|
65
72
|
"drizzle-orm": "0.45.2",
|
|
66
73
|
"jose": "6.2.12"
|
|
67
74
|
},
|
|
68
75
|
"devDependencies": {
|
|
69
|
-
"@rimelight/config": "
|
|
76
|
+
"@rimelight/config": "0.0.13",
|
|
70
77
|
"@types/node": "26.5.1"
|
|
71
78
|
},
|
|
72
79
|
"peerDependencies": {
|
|
@@ -84,5 +91,8 @@
|
|
|
84
91
|
"engines": {
|
|
85
92
|
"node": ">=26.8.2"
|
|
86
93
|
},
|
|
87
|
-
"
|
|
88
|
-
|
|
94
|
+
"scripts": {
|
|
95
|
+
"build": "vp pack",
|
|
96
|
+
"check": "vp check --fix"
|
|
97
|
+
}
|
|
98
|
+
}
|