@xtaskjs/security 1.0.0 → 1.0.2

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/src/strategies.ts DELETED
@@ -1,227 +0,0 @@
1
- import { createDecipheriv } from "crypto";
2
- import { Strategy as PassportStrategyBase } from "passport-strategy";
3
- import {
4
- RegisteredJweSecurityStrategyOptions,
5
- RegisteredSecurityStrategyOptions,
6
- SecurityValidationContext,
7
- } from "./types";
8
-
9
- export const defaultBearerTokenExtractor = (request: any): string | null => {
10
- const headerValue =
11
- request?.headers?.authorization ||
12
- request?.headers?.Authorization ||
13
- request?.authorization;
14
-
15
- if (typeof headerValue !== "string") {
16
- return null;
17
- }
18
-
19
- const match = headerValue.match(/^Bearer\s+(.+)$/i);
20
- return match?.[1] || null;
21
- };
22
-
23
- const decodeBase64Url = (value: string): Buffer => {
24
- const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
25
- const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
26
- return Buffer.from(`${normalized}${padding}`, "base64");
27
- };
28
-
29
- const decodeJsonSegment = (value: string): Record<string, any> => {
30
- return ensureRecord(JSON.parse(decodeBase64Url(value).toString("utf-8")));
31
- };
32
-
33
- const normalizeDecryptionKey = (value: any): Buffer => {
34
- if (Buffer.isBuffer(value)) {
35
- return value;
36
- }
37
-
38
- if (value instanceof Uint8Array) {
39
- return Buffer.from(value);
40
- }
41
-
42
- return Buffer.from(String(value), "utf-8");
43
- };
44
-
45
- const toUnixTime = (): number => Math.floor(Date.now() / 1000);
46
-
47
- const toClockToleranceSeconds = (value?: number | string): number => {
48
- if (typeof value === "number" && Number.isFinite(value)) {
49
- return value;
50
- }
51
-
52
- if (typeof value === "string") {
53
- const parsed = Number(value);
54
- return Number.isFinite(parsed) ? parsed : 0;
55
- }
56
-
57
- return 0;
58
- };
59
-
60
- const normalizeAudience = (value: unknown): string[] => {
61
- if (Array.isArray(value)) {
62
- return value.map((item) => String(item));
63
- }
64
- if (typeof value === "string") {
65
- return [value];
66
- }
67
- return [];
68
- };
69
-
70
- const validateJweClaims = (
71
- payload: Record<string, any>,
72
- definition: RegisteredJweSecurityStrategyOptions
73
- ): void => {
74
- const now = toUnixTime();
75
- const clockTolerance = toClockToleranceSeconds(definition.clockTolerance);
76
-
77
- if (typeof payload.exp === "number" && now - clockTolerance >= payload.exp) {
78
- throw new Error("Token expired");
79
- }
80
-
81
- if (typeof payload.nbf === "number" && now + clockTolerance < payload.nbf) {
82
- throw new Error("Token is not active yet");
83
- }
84
-
85
- if (definition.issuer) {
86
- const expectedIssuers = Array.isArray(definition.issuer)
87
- ? definition.issuer
88
- : [definition.issuer];
89
- if (!expectedIssuers.includes(String(payload.iss || ""))) {
90
- throw new Error("Token issuer is not allowed");
91
- }
92
- }
93
-
94
- if (definition.audience) {
95
- const expectedAudiences = Array.isArray(definition.audience)
96
- ? definition.audience
97
- : [definition.audience];
98
- const actualAudiences = normalizeAudience(payload.aud);
99
- const hasAudience = expectedAudiences.some((audience) => actualAudiences.includes(audience));
100
- if (!hasAudience) {
101
- throw new Error("Token audience is not allowed");
102
- }
103
- }
104
- };
105
-
106
- const ensureRecord = (value: any): Record<string, any> => {
107
- if (value && typeof value === "object" && !Array.isArray(value)) {
108
- return value;
109
- }
110
- return {};
111
- };
112
-
113
- export const resolveValidatedUser = async (
114
- definition: RegisteredSecurityStrategyOptions,
115
- payload: Record<string, any>,
116
- context: SecurityValidationContext
117
- ): Promise<any | false> => {
118
- if (!definition.validate) {
119
- return payload;
120
- }
121
-
122
- return definition.validate(payload, context);
123
- };
124
-
125
- const toJweKey = async (definition: RegisteredJweSecurityStrategyOptions): Promise<any> => {
126
- const key =
127
- typeof definition.resolveDecryptionKey === "function"
128
- ? await definition.resolveDecryptionKey()
129
- : definition.decryptionKey;
130
-
131
- if (typeof key === "string") {
132
- return new TextEncoder().encode(key);
133
- }
134
-
135
- return key;
136
- };
137
-
138
- export class JweStrategy extends PassportStrategyBase {
139
- declare public readonly name: string;
140
- declare public success: (user: any, info?: any) => void;
141
- declare public fail: (challenge?: any, status?: number) => void;
142
- declare public error: (error: Error) => void;
143
- declare public pass: () => void;
144
- private readonly definition: RegisteredJweSecurityStrategyOptions;
145
- private readonly resolveValidationContext?: () => Partial<SecurityValidationContext>;
146
-
147
- constructor(
148
- definition: RegisteredJweSecurityStrategyOptions,
149
- resolveValidationContext?: () => Partial<SecurityValidationContext>
150
- ) {
151
- super();
152
- this.definition = definition;
153
- this.name = definition.name;
154
- this.resolveValidationContext = resolveValidationContext;
155
- }
156
-
157
- authenticate(request: any): void {
158
- void this.run(request);
159
- }
160
-
161
- private async run(request: any): Promise<void> {
162
- const extractor = this.definition.jwtFromRequest || defaultBearerTokenExtractor;
163
- const token = extractor(request);
164
-
165
- if (!token) {
166
- this.fail({ message: "Missing bearer token" }, 401);
167
- return;
168
- }
169
-
170
- try {
171
- const [protectedHeader, encryptedKey, iv, ciphertext, tag] = token.split(".");
172
- if (!protectedHeader || encryptedKey === undefined || !iv || !ciphertext || !tag) {
173
- throw new Error("Malformed JWE token");
174
- }
175
-
176
- const header = decodeJsonSegment(protectedHeader);
177
- if (header.alg !== "dir") {
178
- throw new Error(`Unsupported JWE alg '${String(header.alg || "")}'`);
179
- }
180
-
181
- if (header.enc !== "A256GCM") {
182
- throw new Error(`Unsupported JWE enc '${String(header.enc || "")}'`);
183
- }
184
-
185
- if (encryptedKey.length > 0) {
186
- throw new Error("Compact JWE with direct encryption must not include an encrypted key segment");
187
- }
188
-
189
- const key = normalizeDecryptionKey(await toJweKey(this.definition));
190
- if (key.length !== 32) {
191
- throw new Error("JWE decryption key must be 32 bytes for dir/A256GCM tokens");
192
- }
193
-
194
- const decipher = createDecipheriv("aes-256-gcm", key, decodeBase64Url(iv));
195
- decipher.setAAD(Buffer.from(protectedHeader, "utf-8"));
196
- decipher.setAuthTag(decodeBase64Url(tag));
197
- const plaintext = Buffer.concat([
198
- decipher.update(decodeBase64Url(ciphertext)),
199
- decipher.final(),
200
- ]);
201
- const payload = ensureRecord(JSON.parse(plaintext.toString("utf-8")));
202
- validateJweClaims(payload, this.definition);
203
- const user = await resolveValidatedUser(this.definition, payload, {
204
- ...this.resolveValidationContext?.(),
205
- request,
206
- token,
207
- strategyName: this.definition.name,
208
- kind: this.definition.kind,
209
- });
210
-
211
- if (!user) {
212
- this.fail({ message: "Unauthorized", claims: payload }, 401);
213
- return;
214
- }
215
-
216
- this.success(user, {
217
- claims: payload,
218
- protectedHeader: header,
219
- strategy: this.definition.name,
220
- kind: this.definition.kind,
221
- });
222
- } catch (error: any) {
223
- const message = typeof error?.message === "string" ? error.message : "Unauthorized";
224
- this.fail({ message }, 401);
225
- }
226
- }
227
- }
package/src/tokens.ts DELETED
@@ -1,16 +0,0 @@
1
- export const SECURITY_PASSPORT_TOKEN = "xtask:security:passport";
2
- export const SECURITY_LIFECYCLE_TOKEN = "xtask:security:lifecycle";
3
- export const SECURITY_AUTHENTICATION_SERVICE_TOKEN = "xtask:security:authentication-service";
4
- export const SECURITY_AUTHORIZATION_SERVICE_TOKEN = "xtask:security:authorization-service";
5
-
6
- export const getPassportToken = (): string => SECURITY_PASSPORT_TOKEN;
7
-
8
- export const getSecurityLifecycleToken = (): string => SECURITY_LIFECYCLE_TOKEN;
9
-
10
- export const getAuthenticationServiceToken = (): string => {
11
- return SECURITY_AUTHENTICATION_SERVICE_TOKEN;
12
- };
13
-
14
- export const getAuthorizationServiceToken = (): string => {
15
- return SECURITY_AUTHORIZATION_SERVICE_TOKEN;
16
- };
package/src/types.ts DELETED
@@ -1,94 +0,0 @@
1
- import type { Container } from "@xtaskjs/core";
2
-
3
- export type SecurityStrategyKind = "jwt" | "jwe";
4
- export type SecurityRoleMatchingMode = "any" | "all";
5
- export type SecurityTokenExtractor = (request: any) => string | null | undefined;
6
- export type SecurityRoleExtractor = (payload: Record<string, any>, user: any) => string[] | undefined;
7
-
8
- export interface SecurityValidationContext {
9
- request: any;
10
- response?: any;
11
- token: string;
12
- strategyName: string;
13
- kind: SecurityStrategyKind;
14
- container?: Container;
15
- }
16
-
17
- export type SecurityValidateFn = (
18
- payload: Record<string, any>,
19
- context: SecurityValidationContext
20
- ) => any | false | Promise<any | false>;
21
-
22
- interface BaseSecurityStrategyOptions {
23
- name?: string;
24
- default?: boolean;
25
- jwtFromRequest?: SecurityTokenExtractor;
26
- extractRoles?: SecurityRoleExtractor;
27
- validate?: SecurityValidateFn;
28
- }
29
-
30
- export interface JwtSecurityStrategyOptions extends BaseSecurityStrategyOptions {
31
- kind?: "jwt";
32
- secretOrKey?: any;
33
- secretOrKeyProvider?: any;
34
- issuer?: string | string[];
35
- audience?: string | string[];
36
- algorithms?: string[];
37
- ignoreExpiration?: boolean;
38
- jsonWebTokenOptions?: Record<string, any>;
39
- passReqToCallback?: boolean;
40
- }
41
-
42
- export interface JweSecurityStrategyOptions extends BaseSecurityStrategyOptions {
43
- kind?: "jwe";
44
- decryptionKey?: any;
45
- resolveDecryptionKey?: () => any | Promise<any>;
46
- issuer?: string | string[];
47
- audience?: string | string[];
48
- clockTolerance?: number | string;
49
- }
50
-
51
- export type SecurityStrategyOptions = JwtSecurityStrategyOptions | JweSecurityStrategyOptions;
52
-
53
- export interface RegisteredJwtSecurityStrategyOptions extends JwtSecurityStrategyOptions {
54
- kind: "jwt";
55
- name: string;
56
- }
57
-
58
- export interface RegisteredJweSecurityStrategyOptions extends JweSecurityStrategyOptions {
59
- kind: "jwe";
60
- name: string;
61
- }
62
-
63
- export type RegisteredSecurityStrategyOptions =
64
- | RegisteredJwtSecurityStrategyOptions
65
- | RegisteredJweSecurityStrategyOptions;
66
-
67
- export interface ResolvedSecurityMetadata {
68
- allowAnonymous: boolean;
69
- strategies: string[];
70
- roles: string[];
71
- roleMode: SecurityRoleMatchingMode;
72
- }
73
-
74
- export interface AuthenticatedOptions {
75
- strategies?: string | string[];
76
- }
77
-
78
- export interface RolesOptions {
79
- roles: string[];
80
- mode?: SecurityRoleMatchingMode;
81
- strategies?: string | string[];
82
- }
83
-
84
- export interface SecurityAuthenticationResult {
85
- success: boolean;
86
- statusCode?: number;
87
- challenge?: any;
88
- strategy?: string;
89
- token?: string;
90
- user?: any;
91
- claims?: Record<string, any>;
92
- roles: string[];
93
- info?: any;
94
- }
@@ -1,325 +0,0 @@
1
- import "reflect-metadata";
2
- import { createCipheriv, randomBytes } from "crypto";
3
- import { describe, beforeEach, afterEach, expect, test } from "@jest/globals";
4
- import { Controller, Get } from "@xtaskjs/common";
5
- import { ApplicationLifeCycle, Container, registerControllerRoutes } from "@xtaskjs/core";
6
- import {
7
- AllowAnonymous,
8
- Authenticated,
9
- Auth,
10
- InjectAuthenticationService,
11
- Roles,
12
- SecurityAuthenticationService,
13
- clearRegisteredSecurityStrategies,
14
- getAuthenticationServiceToken,
15
- initializeSecurityIntegration,
16
- registerJweStrategy,
17
- registerJwtStrategy,
18
- shutdownSecurityIntegration,
19
- } from "../src";
20
-
21
- const jwtSecret = new TextEncoder().encode("jwt-secret-for-tests-1234567890");
22
- const jweSecret = new TextEncoder().encode("0123456789abcdef0123456789abcdef");
23
-
24
- const encodeBase64Url = (value: Buffer | string): string => {
25
- return Buffer.from(value)
26
- .toString("base64")
27
- .replace(/\+/g, "-")
28
- .replace(/\//g, "_")
29
- .replace(/=+$/g, "");
30
- };
31
-
32
- const createDirA256GcmJwe = (payload: Record<string, any>, key: Uint8Array): string => {
33
- const protectedHeader = { alg: "dir", enc: "A256GCM" };
34
- const protectedHeaderSegment = encodeBase64Url(JSON.stringify(protectedHeader));
35
- const iv = randomBytes(12);
36
- const cipher = createCipheriv("aes-256-gcm", Buffer.from(key), iv);
37
- cipher.setAAD(Buffer.from(protectedHeaderSegment, "utf-8"));
38
- const ciphertext = Buffer.concat([
39
- cipher.update(Buffer.from(JSON.stringify(payload), "utf-8")),
40
- cipher.final(),
41
- ]);
42
- const tag = cipher.getAuthTag();
43
-
44
- return [
45
- protectedHeaderSegment,
46
- "",
47
- encodeBase64Url(iv),
48
- encodeBase64Url(ciphertext),
49
- encodeBase64Url(tag),
50
- ].join(".");
51
- };
52
-
53
- describe("@xtaskjs/security integration", () => {
54
- beforeEach(async () => {
55
- clearRegisteredSecurityStrategies();
56
- await shutdownSecurityIntegration();
57
- });
58
-
59
- afterEach(async () => {
60
- clearRegisteredSecurityStrategies();
61
- await shutdownSecurityIntegration();
62
- });
63
-
64
- test("authenticates JWT requests and registers services in the container", async () => {
65
- const jsonwebtoken = require("jsonwebtoken");
66
-
67
- class UserDirectory {
68
- private readonly users = new Map<string, { id: string; roles: string[]; active: boolean }>([
69
- ["alice", { id: "alice", roles: ["admin"], active: true }],
70
- ["blocked", { id: "blocked", roles: ["admin"], active: false }],
71
- ]);
72
-
73
- findActiveUser(id: string) {
74
- const user = this.users.get(id);
75
- if (!user || !user.active) {
76
- return undefined;
77
- }
78
- return user;
79
- }
80
- }
81
-
82
- registerJwtStrategy({
83
- name: "default",
84
- default: true,
85
- secretOrKey: jwtSecret,
86
- validate: async (payload, context) => {
87
- if (payload.tenant !== "xtaskjs") {
88
- return false;
89
- }
90
-
91
- const directory = context.container?.get(UserDirectory);
92
- const user = directory?.findActiveUser(String(payload.sub || ""));
93
- if (!user) {
94
- return false;
95
- }
96
-
97
- return {
98
- id: user.id,
99
- sub: user.id,
100
- roles: user.roles,
101
- claims: payload,
102
- };
103
- },
104
- });
105
-
106
- const container = new Container();
107
- container.register(UserDirectory, { scope: "singleton" });
108
- await initializeSecurityIntegration(container);
109
-
110
- class SecurityConsumer {
111
- constructor(
112
- @InjectAuthenticationService()
113
- public readonly authentication: SecurityAuthenticationService
114
- ) {}
115
- }
116
-
117
- container.register(SecurityConsumer, { scope: "singleton" });
118
-
119
- const injected = container.get(SecurityConsumer);
120
- const byName = container.getByName<SecurityAuthenticationService>(getAuthenticationServiceToken());
121
-
122
- expect(injected.authentication).toBeInstanceOf(SecurityAuthenticationService);
123
- expect(byName).toBeInstanceOf(SecurityAuthenticationService);
124
-
125
- @Controller("secure")
126
- class SecureController {
127
- @Get("/profile")
128
- @Authenticated()
129
- @Roles("admin")
130
- profile(req: any) {
131
- return {
132
- sub: req.user.sub,
133
- roles: req.auth.roles,
134
- };
135
- }
136
-
137
- @Get("/health")
138
- @AllowAnonymous()
139
- health() {
140
- return { ok: true };
141
- }
142
- }
143
-
144
- const token = jsonwebtoken.sign(
145
- { sub: "alice", tenant: "xtaskjs", roles: ["admin"] },
146
- Buffer.from(jwtSecret),
147
- { algorithm: "HS256", expiresIn: "2h" }
148
- );
149
-
150
- const lifecycle = new ApplicationLifeCycle();
151
- registerControllerRoutes(new SecureController(), lifecycle);
152
-
153
- const request: any = { headers: { authorization: `Bearer ${token}` } };
154
- const response: any = {};
155
-
156
- const result = await lifecycle.dispatchControllerRoute(
157
- "GET",
158
- "/secure/profile",
159
- request,
160
- response
161
- );
162
-
163
- expect(result).toEqual({ sub: "alice", roles: ["admin"] });
164
- expect(request.auth.isAuthenticated).toBe(true);
165
- expect(request.auth.strategy).toBe("default");
166
- expect(request.user.claims.tenant).toBe("xtaskjs");
167
-
168
- const publicResult = await lifecycle.dispatchControllerRoute("GET", "/secure/health", {}, {});
169
- expect(publicResult).toEqual({ ok: true });
170
- });
171
-
172
- test("uses validate callbacks for DI-backed user lookup and claim validation", async () => {
173
- const jsonwebtoken = require("jsonwebtoken");
174
-
175
- class AccountService {
176
- private readonly accounts = new Map<string, { id: string; roles: string[]; active: boolean }>([
177
- ["sarah", { id: "sarah", roles: ["editor"], active: true }],
178
- ]);
179
-
180
- findById(id: string) {
181
- return this.accounts.get(id);
182
- }
183
- }
184
-
185
- registerJwtStrategy({
186
- name: "lookup",
187
- default: true,
188
- secretOrKey: jwtSecret,
189
- validate: async (payload, context) => {
190
- if (payload.tenant !== "docs") {
191
- return false;
192
- }
193
-
194
- const service = context.container?.get(AccountService);
195
- const account = service?.findById(String(payload.sub || ""));
196
- if (!account || !account.active) {
197
- return false;
198
- }
199
-
200
- return {
201
- sub: account.id,
202
- roles: account.roles,
203
- claims: payload,
204
- };
205
- },
206
- });
207
-
208
- const container = new Container();
209
- container.register(AccountService, { scope: "singleton" });
210
- await initializeSecurityIntegration(container);
211
-
212
- @Controller("docs")
213
- class DocsController {
214
- @Get("/me")
215
- @Authenticated("lookup")
216
- @Roles("editor")
217
- me(req: any) {
218
- return {
219
- user: req.user.sub,
220
- tenant: req.user.claims.tenant,
221
- };
222
- }
223
- }
224
-
225
- const validToken = jsonwebtoken.sign(
226
- { sub: "sarah", tenant: "docs" },
227
- Buffer.from(jwtSecret),
228
- { algorithm: "HS256", expiresIn: "2h" }
229
- );
230
- const invalidTenantToken = jsonwebtoken.sign(
231
- { sub: "sarah", tenant: "other" },
232
- Buffer.from(jwtSecret),
233
- { algorithm: "HS256", expiresIn: "2h" }
234
- );
235
-
236
- const lifecycle = new ApplicationLifeCycle();
237
- registerControllerRoutes(new DocsController(), lifecycle);
238
-
239
- await expect(
240
- lifecycle.dispatchControllerRoute(
241
- "GET",
242
- "/docs/me",
243
- { headers: { authorization: `Bearer ${invalidTenantToken}` } },
244
- {}
245
- )
246
- ).rejects.toMatchObject({ statusCode: 401 });
247
-
248
- const result = await lifecycle.dispatchControllerRoute(
249
- "GET",
250
- "/docs/me",
251
- { headers: { authorization: `Bearer ${validToken}` } },
252
- {}
253
- );
254
-
255
- expect(result).toEqual({ user: "sarah", tenant: "docs" });
256
- });
257
-
258
- test("authenticates JWE requests and rejects missing roles", async () => {
259
- registerJweStrategy({
260
- name: "encrypted",
261
- default: true,
262
- decryptionKey: jweSecret,
263
- });
264
-
265
- await initializeSecurityIntegration();
266
-
267
- @Controller("reports")
268
- class ReportsController {
269
- @Get("/admin")
270
- @Auth("encrypted")
271
- @Roles({ roles: ["admin"], mode: "all" })
272
- admin(req: any) {
273
- return { user: req.user.sub };
274
- }
275
- }
276
-
277
- const token = createDirA256GcmJwe(
278
- {
279
- sub: "bob",
280
- roles: ["viewer"],
281
- iat: Math.floor(Date.now() / 1000),
282
- exp: Math.floor(Date.now() / 1000) + 60 * 60,
283
- },
284
- jweSecret
285
- );
286
-
287
- const lifecycle = new ApplicationLifeCycle();
288
- registerControllerRoutes(new ReportsController(), lifecycle);
289
-
290
- await expect(
291
- lifecycle.dispatchControllerRoute(
292
- "GET",
293
- "/reports/admin",
294
- { headers: { authorization: `Bearer ${token}` } },
295
- {}
296
- )
297
- ).rejects.toMatchObject({ statusCode: 403 });
298
- });
299
-
300
- test("rejects requests without credentials", async () => {
301
- registerJwtStrategy({
302
- name: "default",
303
- default: true,
304
- secretOrKey: jwtSecret,
305
- });
306
-
307
- await initializeSecurityIntegration();
308
-
309
- @Controller("users")
310
- class UsersController {
311
- @Get("/me")
312
- @Authenticated()
313
- me() {
314
- return { ok: true };
315
- }
316
- }
317
-
318
- const lifecycle = new ApplicationLifeCycle();
319
- registerControllerRoutes(new UsersController(), lifecycle);
320
-
321
- await expect(lifecycle.dispatchControllerRoute("GET", "/users/me", {}, {})).rejects.toMatchObject({
322
- statusCode: 401,
323
- });
324
- });
325
- });
package/tsconfig.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2021",
4
- "module": "commonjs",
5
- "rootDir": "./src",
6
- "outDir": "dist",
7
- "esModuleInterop": true,
8
- "experimentalDecorators": true,
9
- "emitDecoratorMetadata": true,
10
- "baseUrl": ".",
11
- "paths": {
12
- "@xtaskjs/common": ["../../dist/packages/common"],
13
- "@xtaskjs/common/*": ["../../dist/packages/common/*"],
14
- "@xtaskjs/core": ["../../dist/packages/core"],
15
- "@xtaskjs/core/*": ["../../dist/packages/core/*"]
16
- },
17
- "declaration": true
18
- },
19
- "include": ["src/**/*"],
20
- "exclude": ["node_modules", "dist"]
21
- }
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2021",
4
- "module": "commonjs",
5
- "esModuleInterop": true,
6
- "experimentalDecorators": true,
7
- "emitDecoratorMetadata": true,
8
- "baseUrl": ".",
9
- "paths": {
10
- "@xtaskjs/common": ["../common/src"],
11
- "@xtaskjs/common/*": ["../common/src/*"],
12
- "@xtaskjs/core": ["../core/src"],
13
- "@xtaskjs/core/*": ["../core/src/*"]
14
- }
15
- },
16
- "include": ["src/**/*", "test/**/*", "../common/src/**/*", "../core/src/**/*"]
17
- }