@zudojs/auth-oauth 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/dist/oauthClient/oauthAuthorize.core.js +6 -4
- package/dist/oauthClient/oauthConfig.resolve.d.ts +10 -0
- package/dist/oauthClient/oauthConfig.resolve.js +20 -5
- package/dist/oauthClient/oauthHttp.core.js +22 -7
- package/dist/oauthErrors/oauthError.base.d.ts +8 -9
- package/dist/oauthErrors/oauthError.base.js +9 -15
- package/dist/oauthSecurity/oauthUrl.guard.js +22 -33
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -5,7 +5,15 @@ default, mandatory anti-CSRF `state` with a timing-safe check, an SSRF guard on
|
|
|
5
5
|
every endpoint URL, a redirect-URI allowlist, and size- and time-bounded
|
|
6
6
|
requests to the provider.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
<!-- zudo-docs:start -->
|
|
9
|
+
|
|
10
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-auth-oauth](https://zudojs.oyinlola.site/docs/packages-auth-oauth) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-auth-oauth.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
11
|
+
|
|
12
|
+
<!-- zudo-docs:end -->
|
|
13
|
+
|
|
14
|
+
Built on Node built-ins (`node:crypto` and the global `fetch`), plus
|
|
15
|
+
`@zudojs/errors` (the error base classes) and `@zudojs/security` (the IPv6
|
|
16
|
+
helpers behind the SSRF guard).
|
|
9
17
|
|
|
10
18
|
```bash
|
|
11
19
|
pnpm add @zudojs/auth-oauth
|
|
@@ -169,7 +177,11 @@ Every one of these is covered by a test in `tests/`.
|
|
|
169
177
|
endpoints (token, user-info) additionally may not point at a loopback,
|
|
170
178
|
private, CGNAT, link-local, unique-local, multicast or reserved address, at
|
|
171
179
|
`169.254.169.254` and friends, or at a `localhost` / `*.local` / `*.internal`
|
|
172
|
-
/ `metadata.google.internal` name.
|
|
180
|
+
/ `metadata.google.internal` name. IPv6 literals that embed an IPv4
|
|
181
|
+
address (`[::127.0.0.1]`, `[::ffff:0:a9fe:a9fe]`, NAT64
|
|
182
|
+
`[64:ff9b::169.254.169.254]`, 6to4 `2002::/16`) are judged as that IPv4
|
|
183
|
+
address, and the local-use NAT64 prefix `64:ff9b:1::/48` is refused.
|
|
184
|
+
Redirects are not followed
|
|
173
185
|
(`redirect: "manual"`), so a 3xx cannot walk the request somewhere that never
|
|
174
186
|
passed the guard.
|
|
175
187
|
**Limit:** the check is on the literal host; DNS is not resolved, so DNS
|
|
@@ -198,6 +210,9 @@ Every one of these is covered by a test in `tests/`.
|
|
|
198
210
|
|
|
199
211
|
Every failure is an `OAuthError` with a machine-readable `code`, a suggested
|
|
200
212
|
`statusCode`, and `expose` saying whether the message is safe to show a user.
|
|
213
|
+
`OAuthError` extends the shared `OAuthError` from `@zudojs/errors`, so every
|
|
214
|
+
class below is also a `BaseError`, and its codes equal the shared
|
|
215
|
+
`ErrorCode.OAUTH_*` members.
|
|
201
216
|
|
|
202
217
|
| Class | Code | Status | Exposed |
|
|
203
218
|
| --- | --- | --- | --- |
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { OAuthConfigurationError } from "../oauthErrors/index.js";
|
|
7
7
|
import { assertValidCodeVerifier, deriveCodeChallenge, generateCodeVerifier, } from "../oauthSecurity/index.js";
|
|
8
|
-
import { assertRedirectUriAllowed, resolveAuthorizeUrl, resolveConfig, } from "./oauthConfig.resolve.js";
|
|
8
|
+
import { assertRedirectUriAllowed, assertScopes, resolveAuthorizeUrl, resolveConfig, } from "./oauthConfig.resolve.js";
|
|
9
9
|
/** Parameters the caller may not override through `additionalParams`. */
|
|
10
10
|
const RESERVED_PARAMS = new Set([
|
|
11
11
|
"response_type",
|
|
@@ -56,9 +56,11 @@ export function createAuthorizationUrl(config, options) {
|
|
|
56
56
|
const codeVerifier = options.codeVerifier ?? generateCodeVerifier();
|
|
57
57
|
assertValidCodeVerifier(codeVerifier);
|
|
58
58
|
const codeChallenge = deriveCodeChallenge(codeVerifier);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
let scopes = resolved.scopes;
|
|
60
|
+
if (options.scopes !== undefined && options.scopes.length > 0) {
|
|
61
|
+
assertScopes(options.scopes);
|
|
62
|
+
scopes = options.scopes;
|
|
63
|
+
}
|
|
62
64
|
const params = new URLSearchParams(url.search);
|
|
63
65
|
for (const [key, value] of Object.entries(resolved.preset.authorizeParams ?? {})) {
|
|
64
66
|
params.set(key, value);
|
|
@@ -32,6 +32,16 @@ export interface ResolvedOAuthConfig {
|
|
|
32
32
|
readonly allowedRedirectUris: readonly string[];
|
|
33
33
|
readonly source: OAuthConfig;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Reject anything that is not a list of RFC 6749 §3.3 scope-tokens.
|
|
37
|
+
*
|
|
38
|
+
* Applied to `config.scopes` and to the per-request `scopes` override on
|
|
39
|
+
* {@link createAuthorizationUrl}, which previously skipped it — so a blank
|
|
40
|
+
* or space-containing entry reached the `scope` parameter untouched.
|
|
41
|
+
*
|
|
42
|
+
* @throws {OAuthConfigurationError} On a non-string or malformed entry.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertScopes(scopes: readonly unknown[]): void;
|
|
35
45
|
/**
|
|
36
46
|
* Validate an `OAuthConfig` and merge it with its provider preset.
|
|
37
47
|
*
|
|
@@ -36,6 +36,25 @@ function boundedInt(value, fallback, min, max, field) {
|
|
|
36
36
|
}
|
|
37
37
|
return value;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Reject anything that is not a list of RFC 6749 §3.3 scope-tokens.
|
|
41
|
+
*
|
|
42
|
+
* Applied to `config.scopes` and to the per-request `scopes` override on
|
|
43
|
+
* {@link createAuthorizationUrl}, which previously skipped it — so a blank
|
|
44
|
+
* or space-containing entry reached the `scope` parameter untouched.
|
|
45
|
+
*
|
|
46
|
+
* @throws {OAuthConfigurationError} On a non-string or malformed entry.
|
|
47
|
+
*/
|
|
48
|
+
export function assertScopes(scopes) {
|
|
49
|
+
if (!Array.isArray(scopes)) {
|
|
50
|
+
throw new OAuthConfigurationError("scopes must be an array of scope-tokens.");
|
|
51
|
+
}
|
|
52
|
+
for (const scope of scopes) {
|
|
53
|
+
if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
|
|
54
|
+
throw new OAuthConfigurationError("Each scope must be a non-empty RFC 6749 scope-token.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Strip a URL's fragment and normalise scheme/host casing for comparison.
|
|
41
60
|
*/
|
|
@@ -66,11 +85,7 @@ export function resolveConfig(config) {
|
|
|
66
85
|
const scopes = config.scopes !== undefined && config.scopes.length > 0
|
|
67
86
|
? config.scopes
|
|
68
87
|
: preset.defaultScopes;
|
|
69
|
-
|
|
70
|
-
if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
|
|
71
|
-
throw new OAuthConfigurationError("Each scope must be a non-empty RFC 6749 scope-token.");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
88
|
+
assertScopes(scopes);
|
|
74
89
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
75
90
|
if (typeof fetchImpl !== "function") {
|
|
76
91
|
throw new OAuthConfigurationError("No fetch implementation available; supply config.fetch.");
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - **Reach** — `redirect: "manual"`, so a 3xx cannot walk the request to a
|
|
14
14
|
* host that never passed the SSRF guard.
|
|
15
15
|
*/
|
|
16
|
-
import { OAuthNetworkError, OAuthProviderError, OAuthResponseError, OAuthResponseTooLargeError, } from "../oauthErrors/index.js";
|
|
16
|
+
import { OAuthError, OAuthNetworkError, OAuthProviderError, OAuthResponseError, OAuthResponseTooLargeError, } from "../oauthErrors/index.js";
|
|
17
17
|
import { parseJsonObject, parseJsonValue } from "../oauthSecurity/index.js";
|
|
18
18
|
/** Provider `error` codes are echoed only if they look like OAuth error codes. */
|
|
19
19
|
const SAFE_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
@@ -94,16 +94,23 @@ export async function requestProviderValue(resolved, request) {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
catch (cause) {
|
|
97
|
-
|
|
98
|
-
const timedOut = name === "TimeoutError" || name === "AbortError";
|
|
99
|
-
throw new OAuthNetworkError(timedOut
|
|
100
|
-
? `The ${request.label} request timed out after ${resolved.timeoutMs}ms.`
|
|
101
|
-
: `The ${request.label} request could not be completed.`, { cause });
|
|
97
|
+
throw toNetworkError(cause, request.label, resolved.timeoutMs);
|
|
102
98
|
}
|
|
103
99
|
if (response.status >= 300 && response.status < 400) {
|
|
104
100
|
throw new OAuthProviderError(`The ${request.label} endpoint returned an unexpected redirect.`, { providerStatus: response.status });
|
|
105
101
|
}
|
|
106
|
-
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = await readCappedText(response, resolved.maxResponseBytes);
|
|
105
|
+
}
|
|
106
|
+
catch (cause) {
|
|
107
|
+
// The timeout signal also aborts the body stream, and a transport can
|
|
108
|
+
// fail mid-body. Both surfaced here as a raw `DOMException` /
|
|
109
|
+
// transport error rather than the documented `OAuthNetworkError`.
|
|
110
|
+
if (cause instanceof OAuthError)
|
|
111
|
+
throw cause;
|
|
112
|
+
throw toNetworkError(cause, request.label, resolved.timeoutMs);
|
|
113
|
+
}
|
|
107
114
|
if (!response.ok) {
|
|
108
115
|
let code;
|
|
109
116
|
try {
|
|
@@ -128,6 +135,14 @@ export async function requestProviderValue(resolved, request) {
|
|
|
128
135
|
}
|
|
129
136
|
return payload;
|
|
130
137
|
}
|
|
138
|
+
/** Wrap a transport or timeout failure in the documented error type. */
|
|
139
|
+
function toNetworkError(cause, label, timeoutMs) {
|
|
140
|
+
const name = cause instanceof Error ? cause.name : "";
|
|
141
|
+
const timedOut = name === "TimeoutError" || name === "AbortError";
|
|
142
|
+
return new OAuthNetworkError(timedOut
|
|
143
|
+
? `The ${label} request timed out after ${timeoutMs}ms.`
|
|
144
|
+
: `The ${label} request could not be completed.`, { cause });
|
|
145
|
+
}
|
|
131
146
|
/** Build the `Authorization: Basic` header for client authentication. */
|
|
132
147
|
export function basicAuthHeader(clientId, clientSecret) {
|
|
133
148
|
// RFC 6749 §2.3.1: both halves are form-urlencoded before base64.
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module oauthErrors/oauthError
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `OAuthError` builds on the shared `OAuthError` in `@zudojs/errors`, so
|
|
7
|
+
* every class here is a `BaseError` (structured `toJSON`, metadata
|
|
8
|
+
* redaction, `category`/`severity`). `OAuthErrorCode` values equal the
|
|
9
|
+
* shared `ErrorCode.OAUTH_*` members.
|
|
8
10
|
*
|
|
9
11
|
* **Secret hygiene.** No constructor here ever interpolates a client secret,
|
|
10
12
|
* an access token, a refresh token or a code verifier into `message`. The
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
* so a provider cannot echo a secret back into your logs. `message` is the
|
|
14
16
|
* first line of `stack`, so keeping it clean keeps the stack clean.
|
|
15
17
|
*/
|
|
18
|
+
import { OAuthError as SharedOAuthError } from "@zudojs/errors";
|
|
16
19
|
/** Stable, machine-readable error codes. */
|
|
17
20
|
export declare const OAuthErrorCode: {
|
|
18
21
|
/** The config is unusable: missing field, bad URL, unsupported operation. */
|
|
@@ -48,16 +51,12 @@ export interface OAuthErrorOptions {
|
|
|
48
51
|
*
|
|
49
52
|
* `expose` says whether the message is safe to hand to an end user; it is
|
|
50
53
|
* `true` for request-caused failures and `false` for configuration ones
|
|
51
|
-
* (which describe your deployment, not the request).
|
|
54
|
+
* (which describe your deployment, not the request). Defaults: 400,
|
|
55
|
+
* exposed, code `OAUTH_PROVIDER_REJECTED`.
|
|
52
56
|
*/
|
|
53
|
-
export declare class OAuthError extends
|
|
54
|
-
readonly name: string;
|
|
57
|
+
export declare class OAuthError extends SharedOAuthError {
|
|
55
58
|
/** Machine-readable code. */
|
|
56
59
|
readonly code: OAuthErrorCode;
|
|
57
|
-
/** Suggested HTTP status for a handler that surfaces this. */
|
|
58
|
-
readonly statusCode: number;
|
|
59
|
-
/** Whether `message` is safe to return to a client verbatim. */
|
|
60
|
-
readonly expose: boolean;
|
|
61
60
|
constructor(message: string, options?: OAuthErrorOptions);
|
|
62
61
|
}
|
|
63
62
|
/** The configuration is missing something or is structurally unusable. */
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module oauthErrors/oauthError
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `OAuthError` builds on the shared `OAuthError` in `@zudojs/errors`, so
|
|
7
|
+
* every class here is a `BaseError` (structured `toJSON`, metadata
|
|
8
|
+
* redaction, `category`/`severity`). `OAuthErrorCode` values equal the
|
|
9
|
+
* shared `ErrorCode.OAUTH_*` members.
|
|
8
10
|
*
|
|
9
11
|
* **Secret hygiene.** No constructor here ever interpolates a client secret,
|
|
10
12
|
* an access token, a refresh token or a code verifier into `message`. The
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
* so a provider cannot echo a secret back into your logs. `message` is the
|
|
14
16
|
* first line of `stack`, so keeping it clean keeps the stack clean.
|
|
15
17
|
*/
|
|
18
|
+
import { OAuthError as SharedOAuthError } from "@zudojs/errors";
|
|
16
19
|
/** Stable, machine-readable error codes. */
|
|
17
20
|
export const OAuthErrorCode = {
|
|
18
21
|
/** The config is unusable: missing field, bad URL, unsupported operation. */
|
|
@@ -39,21 +42,12 @@ export const OAuthErrorCode = {
|
|
|
39
42
|
*
|
|
40
43
|
* `expose` says whether the message is safe to hand to an end user; it is
|
|
41
44
|
* `true` for request-caused failures and `false` for configuration ones
|
|
42
|
-
* (which describe your deployment, not the request).
|
|
45
|
+
* (which describe your deployment, not the request). Defaults: 400,
|
|
46
|
+
* exposed, code `OAUTH_PROVIDER_REJECTED`.
|
|
43
47
|
*/
|
|
44
|
-
export class OAuthError extends
|
|
45
|
-
name = "OAuthError";
|
|
46
|
-
/** Machine-readable code. */
|
|
47
|
-
code;
|
|
48
|
-
/** Suggested HTTP status for a handler that surfaces this. */
|
|
49
|
-
statusCode;
|
|
50
|
-
/** Whether `message` is safe to return to a client verbatim. */
|
|
51
|
-
expose;
|
|
48
|
+
export class OAuthError extends SharedOAuthError {
|
|
52
49
|
constructor(message, options) {
|
|
53
|
-
super(message, options
|
|
54
|
-
this.code = options?.code ?? OAuthErrorCode.PROVIDER_REJECTED;
|
|
55
|
-
this.statusCode = options?.statusCode ?? 400;
|
|
56
|
-
this.expose = options?.expose ?? true;
|
|
50
|
+
super(message, options);
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
53
|
/** The configuration is missing something or is structurally unusable. */
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* rebinding) is not caught here. Pair this with network egress controls if
|
|
27
27
|
* you accept endpoint URLs from untrusted operators.
|
|
28
28
|
*/
|
|
29
|
+
import { embeddedIpv4, expandIpv6, isNonPublicIpv6Range, } from "@zudojs/security";
|
|
29
30
|
import { OAuthEndpointNotAllowedError } from "../oauthErrors/index.js";
|
|
30
31
|
/** Hostnames that are always refused for a server-fetched endpoint. */
|
|
31
32
|
const BLOCKED_HOST_NAMES = new Set([
|
|
@@ -90,40 +91,22 @@ function isNonPublicIpv4(octets) {
|
|
|
90
91
|
return true; // multicast + reserved + broadcast
|
|
91
92
|
return false;
|
|
92
93
|
}
|
|
93
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Whether an IPv6 literal (already stripped of brackets) is non-public.
|
|
96
|
+
*
|
|
97
|
+
* Any form embedding an IPv4 address (compatible `::/96`, mapped, translated,
|
|
98
|
+
* NAT64 `64:ff9b::/96`, 6to4) is judged as that IPv4 address. The WHATWG
|
|
99
|
+
* parser serialises `[::127.0.0.1]` as `[::7f00:1]`, which the old
|
|
100
|
+
* dotted-form regex never matched. An unparseable literal fails closed.
|
|
101
|
+
*/
|
|
94
102
|
function isNonPublicIpv6(raw) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
103
|
+
const groups = expandIpv6(raw);
|
|
104
|
+
if (!groups)
|
|
97
105
|
return true;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const octets = parseIpv4(embedded);
|
|
103
|
-
return octets === undefined ? true : isNonPublicIpv4(octets);
|
|
104
|
-
}
|
|
105
|
-
// The WHATWG URL parser rewrites `::ffff:127.0.0.1` as `::ffff:7f00:1`,
|
|
106
|
-
// so the hex form has to be decoded back to its embedded IPv4 address.
|
|
107
|
-
const hexMapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
108
|
-
const high = hexMapped?.[1];
|
|
109
|
-
const low = hexMapped?.[2];
|
|
110
|
-
if (high !== undefined && low !== undefined) {
|
|
111
|
-
const word1 = Number.parseInt(high, 16);
|
|
112
|
-
const word2 = Number.parseInt(low, 16);
|
|
113
|
-
return isNonPublicIpv4([
|
|
114
|
-
(word1 >> 8) & 0xff,
|
|
115
|
-
word1 & 0xff,
|
|
116
|
-
(word2 >> 8) & 0xff,
|
|
117
|
-
word2 & 0xff,
|
|
118
|
-
]);
|
|
119
|
-
}
|
|
120
|
-
if (/^f[cd][0-9a-f]{2}:/.test(host))
|
|
121
|
-
return true; // fc00::/7 unique local
|
|
122
|
-
if (/^fe[89ab][0-9a-f]:/.test(host))
|
|
123
|
-
return true; // fe80::/10 link-local
|
|
124
|
-
if (/^ff[0-9a-f]{2}:/.test(host))
|
|
125
|
-
return true; // ff00::/8 multicast
|
|
126
|
-
return false;
|
|
106
|
+
const embedded = embeddedIpv4(groups);
|
|
107
|
+
if (embedded)
|
|
108
|
+
return isNonPublicIpv4(embedded);
|
|
109
|
+
return isNonPublicIpv6Range(groups);
|
|
127
110
|
}
|
|
128
111
|
/**
|
|
129
112
|
* Whether a host literal is one this package refuses to fetch from.
|
|
@@ -132,9 +115,15 @@ function isNonPublicIpv6(raw) {
|
|
|
132
115
|
*/
|
|
133
116
|
export function isBlockedFetchHost(hostname) {
|
|
134
117
|
const host = hostname.toLowerCase();
|
|
135
|
-
const
|
|
118
|
+
const unbracketed = host.startsWith("[") && host.endsWith("]")
|
|
136
119
|
? host.slice(1, -1)
|
|
137
120
|
: host;
|
|
121
|
+
// A trailing dot marks a fully-qualified name (`localhost.`,
|
|
122
|
+
// `metadata.google.internal.`). DNS resolves it to the same address as
|
|
123
|
+
// the undotted form, but the WHATWG parser keeps the dot on domain
|
|
124
|
+
// hosts, so without stripping it every name-based rule below was one
|
|
125
|
+
// character away from being bypassed.
|
|
126
|
+
const bare = unbracketed.replace(/\.+$/, "");
|
|
138
127
|
if (BLOCKED_HOST_NAMES.has(bare))
|
|
139
128
|
return true;
|
|
140
129
|
for (const suffix of BLOCKED_HOST_SUFFIXES) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/auth-oauth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "OAuth2 authorization-code client for the Zudojs framework — PKCE S256, mandatory state, SSRF-guarded endpoints, and provider presets for Google, GitHub, Microsoft, Apple and Discord.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -48,6 +52,10 @@
|
|
|
48
52
|
"url": "https://github.com/oyinlola-tech/zudo",
|
|
49
53
|
"directory": "packages/auth-oauth"
|
|
50
54
|
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@zudojs/errors": "1.1.0",
|
|
57
|
+
"@zudojs/security": "1.1.0"
|
|
58
|
+
},
|
|
51
59
|
"scripts": {
|
|
52
60
|
"build": "tsc -p tsconfig.json",
|
|
53
61
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|