@nexpress/core 0.1.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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/audit-54XLVCWD.js +14 -0
- package/dist/audit-54XLVCWD.js.map +1 -0
- package/dist/auth.d.ts +640 -0
- package/dist/auth.js +94 -0
- package/dist/auth.js.map +1 -0
- package/dist/can-YLUHRJAB.js +19 -0
- package/dist/can-YLUHRJAB.js.map +1 -0
- package/dist/chunk-2G264RCD.js +68 -0
- package/dist/chunk-2G264RCD.js.map +1 -0
- package/dist/chunk-2YDGE7YX.js +92 -0
- package/dist/chunk-2YDGE7YX.js.map +1 -0
- package/dist/chunk-473S4TER.js +538 -0
- package/dist/chunk-473S4TER.js.map +1 -0
- package/dist/chunk-4ZLMEKFX.js +18 -0
- package/dist/chunk-4ZLMEKFX.js.map +1 -0
- package/dist/chunk-55FU6WED.js +179 -0
- package/dist/chunk-55FU6WED.js.map +1 -0
- package/dist/chunk-6YI5K2TI.js +1959 -0
- package/dist/chunk-6YI5K2TI.js.map +1 -0
- package/dist/chunk-BHK3AD3Q.js +41 -0
- package/dist/chunk-BHK3AD3Q.js.map +1 -0
- package/dist/chunk-CRUQBZUF.js +39 -0
- package/dist/chunk-CRUQBZUF.js.map +1 -0
- package/dist/chunk-CTSQ7BRI.js +175 -0
- package/dist/chunk-CTSQ7BRI.js.map +1 -0
- package/dist/chunk-DK2JBJH7.js +81 -0
- package/dist/chunk-DK2JBJH7.js.map +1 -0
- package/dist/chunk-DP2PREDU.js +597 -0
- package/dist/chunk-DP2PREDU.js.map +1 -0
- package/dist/chunk-EQ2Z3KMD.js +24 -0
- package/dist/chunk-EQ2Z3KMD.js.map +1 -0
- package/dist/chunk-FZ7O6DWI.js +305 -0
- package/dist/chunk-FZ7O6DWI.js.map +1 -0
- package/dist/chunk-ISLYFQWL.js +1270 -0
- package/dist/chunk-ISLYFQWL.js.map +1 -0
- package/dist/chunk-JJL74ZPK.js +68 -0
- package/dist/chunk-JJL74ZPK.js.map +1 -0
- package/dist/chunk-JKXAPSU4.js +24 -0
- package/dist/chunk-JKXAPSU4.js.map +1 -0
- package/dist/chunk-KU5M27ZC.js +24 -0
- package/dist/chunk-KU5M27ZC.js.map +1 -0
- package/dist/chunk-LSHHRDVR.js +34 -0
- package/dist/chunk-LSHHRDVR.js.map +1 -0
- package/dist/chunk-M43PGOQY.js +715 -0
- package/dist/chunk-M43PGOQY.js.map +1 -0
- package/dist/chunk-MEJAHXIO.js +150 -0
- package/dist/chunk-MEJAHXIO.js.map +1 -0
- package/dist/chunk-NUCGHWCF.js +101 -0
- package/dist/chunk-NUCGHWCF.js.map +1 -0
- package/dist/chunk-OK5HOCQI.js +845 -0
- package/dist/chunk-OK5HOCQI.js.map +1 -0
- package/dist/chunk-OROPGO65.js +13 -0
- package/dist/chunk-OROPGO65.js.map +1 -0
- package/dist/chunk-PPAS4SZR.js +176 -0
- package/dist/chunk-PPAS4SZR.js.map +1 -0
- package/dist/chunk-PPBWRKO2.js +171 -0
- package/dist/chunk-PPBWRKO2.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-QO7LAQZH.js +321 -0
- package/dist/chunk-QO7LAQZH.js.map +1 -0
- package/dist/chunk-QVJ2HCAX.js +225 -0
- package/dist/chunk-QVJ2HCAX.js.map +1 -0
- package/dist/chunk-RIPHIRPP.js +68 -0
- package/dist/chunk-RIPHIRPP.js.map +1 -0
- package/dist/chunk-S27S42QY.js +134 -0
- package/dist/chunk-S27S42QY.js.map +1 -0
- package/dist/chunk-SBCVAC2Z.js +40 -0
- package/dist/chunk-SBCVAC2Z.js.map +1 -0
- package/dist/chunk-TFJ4MKPH.js +694 -0
- package/dist/chunk-TFJ4MKPH.js.map +1 -0
- package/dist/chunk-THX3SHYA.js +75 -0
- package/dist/chunk-THX3SHYA.js.map +1 -0
- package/dist/chunk-UGQSQO5B.js +222 -0
- package/dist/chunk-UGQSQO5B.js.map +1 -0
- package/dist/chunk-V2UNHGAP.js +26 -0
- package/dist/chunk-V2UNHGAP.js.map +1 -0
- package/dist/chunk-VGTPQXNQ.js +2790 -0
- package/dist/chunk-VGTPQXNQ.js.map +1 -0
- package/dist/chunk-VNIHXQ7W.js +194 -0
- package/dist/chunk-VNIHXQ7W.js.map +1 -0
- package/dist/chunk-WV272MPW.js +31 -0
- package/dist/chunk-WV272MPW.js.map +1 -0
- package/dist/chunk-X5KKBOUS.js +26 -0
- package/dist/chunk-X5KKBOUS.js.map +1 -0
- package/dist/chunk-XANPEOJC.js +17 -0
- package/dist/chunk-XANPEOJC.js.map +1 -0
- package/dist/chunk-XPVQIHAQ.js +83 -0
- package/dist/chunk-XPVQIHAQ.js.map +1 -0
- package/dist/chunk-ZCINJSS4.js +75 -0
- package/dist/chunk-ZCINJSS4.js.map +1 -0
- package/dist/community.d.ts +1425 -0
- package/dist/community.js +206 -0
- package/dist/community.js.map +1 -0
- package/dist/config-2GDU7PCK.js +32 -0
- package/dist/config-2GDU7PCK.js.map +1 -0
- package/dist/context-MNZ4QXPC.js +16 -0
- package/dist/context-MNZ4QXPC.js.map +1 -0
- package/dist/db-schema.d.ts +4 -0
- package/dist/db-schema.js +102 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.d.ts +7 -0
- package/dist/db.js +117 -0
- package/dist/db.js.map +1 -0
- package/dist/digest-SY42GQSU.js +17 -0
- package/dist/digest-SY42GQSU.js.map +1 -0
- package/dist/errors-5OS3S2J3.js +22 -0
- package/dist/errors-5OS3S2J3.js.map +1 -0
- package/dist/host-OBOI4MJK.js +51 -0
- package/dist/host-OBOI4MJK.js.map +1 -0
- package/dist/i18n.d.ts +301 -0
- package/dist/i18n.js +68 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index-B6-_vr_m.d.ts +590 -0
- package/dist/index-CY55LC0u.d.ts +4722 -0
- package/dist/index-CeiTvwbp.d.ts +168 -0
- package/dist/index-XwP1ET8b.d.ts +61 -0
- package/dist/index.d.ts +2037 -0
- package/dist/index.js +2205 -0
- package/dist/index.js.map +1 -0
- package/dist/job-log-VZXWQUDK.js +24 -0
- package/dist/job-log-VZXWQUDK.js.map +1 -0
- package/dist/jobs.d.ts +4 -0
- package/dist/jobs.js +76 -0
- package/dist/jobs.js.map +1 -0
- package/dist/logger-DqGaOU_j.d.ts +29 -0
- package/dist/logger-S7REWDNE.js +16 -0
- package/dist/logger-S7REWDNE.js.map +1 -0
- package/dist/media.d.ts +5 -0
- package/dist/media.js +41 -0
- package/dist/media.js.map +1 -0
- package/dist/mentions-2IHFVSHW.js +23 -0
- package/dist/mentions-2IHFVSHW.js.map +1 -0
- package/dist/mutes-EWAE5FZR.js +21 -0
- package/dist/mutes-EWAE5FZR.js.map +1 -0
- package/dist/notification-prefs-VPJDU7I6.js +21 -0
- package/dist/notification-prefs-VPJDU7I6.js.map +1 -0
- package/dist/observability.d.ts +156 -0
- package/dist/observability.js +32 -0
- package/dist/observability.js.map +1 -0
- package/dist/profanity-adapter-NU2JQSLX.js +12 -0
- package/dist/profanity-adapter-NU2JQSLX.js.map +1 -0
- package/dist/queue-XE5BC75T.js +14 -0
- package/dist/queue-XE5BC75T.js.map +1 -0
- package/dist/rate-limit.d.ts +99 -0
- package/dist/rate-limit.js +14 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/registry-XIXDEPVI.js +31 -0
- package/dist/registry-XIXDEPVI.js.map +1 -0
- package/dist/reputation-JRL2YQHM.js +11 -0
- package/dist/reputation-JRL2YQHM.js.map +1 -0
- package/dist/routes.d.ts +43 -0
- package/dist/routes.js +12 -0
- package/dist/routes.js.map +1 -0
- package/dist/scheduled-CIQM57HT.js +20 -0
- package/dist/scheduled-CIQM57HT.js.map +1 -0
- package/dist/seo.d.ts +410 -0
- package/dist/seo.js +44 -0
- package/dist/seo.js.map +1 -0
- package/dist/settings-FOBIESPB.js +17 -0
- package/dist/settings-FOBIESPB.js.map +1 -0
- package/dist/spam-adapter-XX3G737Z.js +12 -0
- package/dist/spam-adapter-XX3G737Z.js.map +1 -0
- package/dist/strings-VAE47B2C.js +29 -0
- package/dist/strings-VAE47B2C.js.map +1 -0
- package/dist/templates-IFVJMCJ6.js +12 -0
- package/dist/templates-IFVJMCJ6.js.map +1 -0
- package/dist/types-TlsbXS0T.d.ts +871 -0
- package/package.json +129 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARGON2_OPTIONS,
|
|
3
|
+
authenticated,
|
|
4
|
+
consumeMemberEmailVerifyToken,
|
|
5
|
+
consumeMemberPasswordReset,
|
|
6
|
+
consumePasswordResetToken,
|
|
7
|
+
createMemberEmailVerifyToken,
|
|
8
|
+
createPasswordResetToken,
|
|
9
|
+
fromArctic,
|
|
10
|
+
getMemberFromTokenPayload,
|
|
11
|
+
getOAuthProvider,
|
|
12
|
+
hashPassword,
|
|
13
|
+
invalidateAllMemberSessions,
|
|
14
|
+
invalidateAllSessions,
|
|
15
|
+
isAdmin,
|
|
16
|
+
isEditorOrAbove,
|
|
17
|
+
isOwnerOrAdmin,
|
|
18
|
+
isTokenVerificationError,
|
|
19
|
+
issueOAuthState,
|
|
20
|
+
listMemberIdentities,
|
|
21
|
+
listOAuthProviders,
|
|
22
|
+
listUserIdentities,
|
|
23
|
+
registerOAuthProvider,
|
|
24
|
+
requestMemberPasswordReset,
|
|
25
|
+
requestPasswordReset,
|
|
26
|
+
resetOAuthProviders,
|
|
27
|
+
resolveMemberOAuthLogin,
|
|
28
|
+
resolveOAuthLogin,
|
|
29
|
+
revokeMemberIdentity,
|
|
30
|
+
revokeUserIdentity,
|
|
31
|
+
sha256,
|
|
32
|
+
signMemberToken,
|
|
33
|
+
signToken,
|
|
34
|
+
verifyCsrf,
|
|
35
|
+
verifyMemberToken,
|
|
36
|
+
verifyOAuthState,
|
|
37
|
+
verifyPassword,
|
|
38
|
+
verifyToken,
|
|
39
|
+
verifyTokenFull
|
|
40
|
+
} from "./chunk-OK5HOCQI.js";
|
|
41
|
+
import {
|
|
42
|
+
can
|
|
43
|
+
} from "./chunk-EQ2Z3KMD.js";
|
|
44
|
+
import "./chunk-RIPHIRPP.js";
|
|
45
|
+
import "./chunk-PPBWRKO2.js";
|
|
46
|
+
import "./chunk-SBCVAC2Z.js";
|
|
47
|
+
import "./chunk-ZCINJSS4.js";
|
|
48
|
+
import "./chunk-OROPGO65.js";
|
|
49
|
+
import "./chunk-JJL74ZPK.js";
|
|
50
|
+
import "./chunk-XANPEOJC.js";
|
|
51
|
+
import "./chunk-M43PGOQY.js";
|
|
52
|
+
import "./chunk-PZ5AY32C.js";
|
|
53
|
+
export {
|
|
54
|
+
ARGON2_OPTIONS,
|
|
55
|
+
authenticated,
|
|
56
|
+
can,
|
|
57
|
+
consumeMemberEmailVerifyToken,
|
|
58
|
+
consumeMemberPasswordReset,
|
|
59
|
+
consumePasswordResetToken,
|
|
60
|
+
createMemberEmailVerifyToken,
|
|
61
|
+
createPasswordResetToken,
|
|
62
|
+
fromArctic,
|
|
63
|
+
getMemberFromTokenPayload,
|
|
64
|
+
getOAuthProvider,
|
|
65
|
+
hashPassword,
|
|
66
|
+
invalidateAllMemberSessions,
|
|
67
|
+
invalidateAllSessions,
|
|
68
|
+
isAdmin,
|
|
69
|
+
isEditorOrAbove,
|
|
70
|
+
isOwnerOrAdmin,
|
|
71
|
+
isTokenVerificationError,
|
|
72
|
+
issueOAuthState,
|
|
73
|
+
listMemberIdentities,
|
|
74
|
+
listOAuthProviders,
|
|
75
|
+
listUserIdentities,
|
|
76
|
+
registerOAuthProvider,
|
|
77
|
+
requestMemberPasswordReset,
|
|
78
|
+
requestPasswordReset,
|
|
79
|
+
resetOAuthProviders,
|
|
80
|
+
resolveMemberOAuthLogin,
|
|
81
|
+
resolveOAuthLogin,
|
|
82
|
+
revokeMemberIdentity,
|
|
83
|
+
revokeUserIdentity,
|
|
84
|
+
sha256,
|
|
85
|
+
signMemberToken,
|
|
86
|
+
signToken,
|
|
87
|
+
verifyCsrf,
|
|
88
|
+
verifyMemberToken,
|
|
89
|
+
verifyOAuthState,
|
|
90
|
+
verifyPassword,
|
|
91
|
+
verifyToken,
|
|
92
|
+
verifyTokenFull
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertNotBanned,
|
|
3
|
+
isMemberBanned,
|
|
4
|
+
memberCan,
|
|
5
|
+
withMemberWrite
|
|
6
|
+
} from "./chunk-55FU6WED.js";
|
|
7
|
+
import "./chunk-FZ7O6DWI.js";
|
|
8
|
+
import "./chunk-SBCVAC2Z.js";
|
|
9
|
+
import "./chunk-ZCINJSS4.js";
|
|
10
|
+
import "./chunk-XANPEOJC.js";
|
|
11
|
+
import "./chunk-M43PGOQY.js";
|
|
12
|
+
import "./chunk-PZ5AY32C.js";
|
|
13
|
+
export {
|
|
14
|
+
assertNotBanned,
|
|
15
|
+
isMemberBanned,
|
|
16
|
+
memberCan,
|
|
17
|
+
withMemberWrite
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=can-YLUHRJAB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/rate-limit/in-memory.ts
|
|
2
|
+
function getStore() {
|
|
3
|
+
let store = globalThis.__nx_rate_limit_store;
|
|
4
|
+
if (!store) {
|
|
5
|
+
store = /* @__PURE__ */ new Map();
|
|
6
|
+
globalThis.__nx_rate_limit_store = store;
|
|
7
|
+
}
|
|
8
|
+
return store;
|
|
9
|
+
}
|
|
10
|
+
function ensureJanitor() {
|
|
11
|
+
if (globalThis.__nx_rate_limit_cleanup_handle) return;
|
|
12
|
+
const handle = setInterval(() => {
|
|
13
|
+
const liveStore = globalThis.__nx_rate_limit_store;
|
|
14
|
+
if (!liveStore) return;
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
for (const [key, bucket] of liveStore) {
|
|
17
|
+
if (now > bucket.resetAt) liveStore.delete(key);
|
|
18
|
+
}
|
|
19
|
+
}, 6e4);
|
|
20
|
+
handle.unref?.();
|
|
21
|
+
globalThis.__nx_rate_limit_cleanup_handle = handle;
|
|
22
|
+
}
|
|
23
|
+
var InMemoryRateLimiter = class {
|
|
24
|
+
constructor() {
|
|
25
|
+
ensureJanitor();
|
|
26
|
+
}
|
|
27
|
+
check(key, limit, windowMs) {
|
|
28
|
+
const store = getStore();
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const bucket = store.get(key);
|
|
31
|
+
if (!bucket || now > bucket.resetAt) {
|
|
32
|
+
store.set(key, { count: 1, resetAt: now + windowMs });
|
|
33
|
+
return Promise.resolve({
|
|
34
|
+
limited: false,
|
|
35
|
+
retryAfterSeconds: Math.ceil(windowMs / 1e3)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
bucket.count += 1;
|
|
39
|
+
const retryAfterSeconds = Math.ceil((bucket.resetAt - now) / 1e3);
|
|
40
|
+
if (bucket.count > limit) {
|
|
41
|
+
return Promise.resolve({ limited: true, retryAfterSeconds });
|
|
42
|
+
}
|
|
43
|
+
return Promise.resolve({ limited: false, retryAfterSeconds });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/rate-limit/registry.ts
|
|
48
|
+
var rateLimiter = null;
|
|
49
|
+
function setRateLimiter(adapter) {
|
|
50
|
+
rateLimiter = adapter;
|
|
51
|
+
}
|
|
52
|
+
function getRateLimiter() {
|
|
53
|
+
if (!rateLimiter) {
|
|
54
|
+
rateLimiter = new InMemoryRateLimiter();
|
|
55
|
+
}
|
|
56
|
+
return rateLimiter;
|
|
57
|
+
}
|
|
58
|
+
function getOptionalRateLimiter() {
|
|
59
|
+
return rateLimiter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
InMemoryRateLimiter,
|
|
64
|
+
setRateLimiter,
|
|
65
|
+
getRateLimiter,
|
|
66
|
+
getOptionalRateLimiter
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=chunk-2G264RCD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rate-limit/in-memory.ts","../src/rate-limit/registry.ts"],"sourcesContent":["import type { NpRateLimitDecision, NpRateLimiterAdapter } from \"./types.js\";\n\n/**\n * Default rate-limiter adapter. Same fixed-window behaviour the\n * proxy already had before this lived behind an interface — a\n * request in the first ms and one in the last ms of a window\n * share the bucket. Single-node deploys keep this; multi-node\n * deploys swap in `@nexpress/rate-limiter-redis` (or another\n * adapter) at boot.\n *\n * Why store + janitor on `globalThis`: HMR re-evaluates this\n * module on every save in dev, and a module-scoped Map would\n * orphan the previous one from its cleanup interval (#315).\n * Pinning both to `globalThis` keeps Map and janitor paired\n * across reloads.\n *\n * Why the janitor arms lazily: importing this file alone\n * shouldn't keep a Node process alive. CLI / one-shot scripts\n * that pull in `@nexpress/core` for unrelated reasons shouldn't\n * inherit a 60-second timer.\n */\n\ninterface Bucket {\n count: number;\n resetAt: number;\n}\n\ndeclare global {\n var __nx_rate_limit_store: Map<string, Bucket> | undefined;\n var __nx_rate_limit_cleanup_handle: NodeJS.Timeout | undefined;\n}\n\nfunction getStore(): Map<string, Bucket> {\n let store = globalThis.__nx_rate_limit_store;\n if (!store) {\n store = new Map<string, Bucket>();\n globalThis.__nx_rate_limit_store = store;\n }\n return store;\n}\n\nfunction ensureJanitor(): void {\n if (globalThis.__nx_rate_limit_cleanup_handle) return;\n const handle = setInterval(() => {\n const liveStore = globalThis.__nx_rate_limit_store;\n if (!liveStore) return;\n const now = Date.now();\n for (const [key, bucket] of liveStore) {\n if (now > bucket.resetAt) liveStore.delete(key);\n }\n }, 60_000);\n // Don't keep a Node process alive just to prune empty buckets.\n handle.unref?.();\n globalThis.__nx_rate_limit_cleanup_handle = handle;\n}\n\nexport class InMemoryRateLimiter implements NpRateLimiterAdapter {\n constructor() {\n ensureJanitor();\n }\n\n check(key: string, limit: number, windowMs: number): Promise<NpRateLimitDecision> {\n const store = getStore();\n const now = Date.now();\n const bucket = store.get(key);\n if (!bucket || now > bucket.resetAt) {\n store.set(key, { count: 1, resetAt: now + windowMs });\n return Promise.resolve({\n limited: false,\n retryAfterSeconds: Math.ceil(windowMs / 1000),\n });\n }\n bucket.count += 1;\n const retryAfterSeconds = Math.ceil((bucket.resetAt - now) / 1000);\n if (bucket.count > limit) {\n return Promise.resolve({ limited: true, retryAfterSeconds });\n }\n return Promise.resolve({ limited: false, retryAfterSeconds });\n }\n}\n\n/**\n * Test-only: clear the in-memory store. Not part of the public\n * surface — tests inside `@nexpress/core` reach for it via the\n * direct file import; downstream consumers shouldn't.\n */\nexport function __resetInMemoryRateLimitStoreForTests(): void {\n globalThis.__nx_rate_limit_store?.clear();\n}\n","import { InMemoryRateLimiter } from \"./in-memory.js\";\nimport type { NpRateLimiterAdapter } from \"./types.js\";\n\n/**\n * Phase 23.7 — singleton registration for the rate limiter, mirroring\n * the existing `setStorageAdapter` / `setJobQueue` pattern. Boot wires\n * the desired adapter once; the proxy reads it on every request via\n * `getRateLimiter()`.\n *\n * `getRateLimiter()` lazily installs the in-memory default the first\n * time it's called without an explicit setup. That keeps single-node\n * deployments zero-config — they get the same behavior they had\n * before the adapter contract existed without touching their boot\n * code.\n */\n\nlet rateLimiter: NpRateLimiterAdapter | null = null;\n\nexport function setRateLimiter(adapter: NpRateLimiterAdapter | null): void {\n rateLimiter = adapter;\n}\n\nexport function getRateLimiter(): NpRateLimiterAdapter {\n if (!rateLimiter) {\n rateLimiter = new InMemoryRateLimiter();\n }\n return rateLimiter;\n}\n\nexport function getOptionalRateLimiter(): NpRateLimiterAdapter | null {\n return rateLimiter;\n}\n"],"mappings":";AAgCA,SAAS,WAAgC;AACvC,MAAI,QAAQ,WAAW;AACvB,MAAI,CAAC,OAAO;AACV,YAAQ,oBAAI,IAAoB;AAChC,eAAW,wBAAwB;AAAA,EACrC;AACA,SAAO;AACT;AAEA,SAAS,gBAAsB;AAC7B,MAAI,WAAW,+BAAgC;AAC/C,QAAM,SAAS,YAAY,MAAM;AAC/B,UAAM,YAAY,WAAW;AAC7B,QAAI,CAAC,UAAW;AAChB,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,CAAC,KAAK,MAAM,KAAK,WAAW;AACrC,UAAI,MAAM,OAAO,QAAS,WAAU,OAAO,GAAG;AAAA,IAChD;AAAA,EACF,GAAG,GAAM;AAET,SAAO,QAAQ;AACf,aAAW,iCAAiC;AAC9C;AAEO,IAAM,sBAAN,MAA0D;AAAA,EAC/D,cAAc;AACZ,kBAAc;AAAA,EAChB;AAAA,EAEA,MAAM,KAAa,OAAe,UAAgD;AAChF,UAAM,QAAQ,SAAS;AACvB,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,SAAS,MAAM,IAAI,GAAG;AAC5B,QAAI,CAAC,UAAU,MAAM,OAAO,SAAS;AACnC,YAAM,IAAI,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,SAAS,CAAC;AACpD,aAAO,QAAQ,QAAQ;AAAA,QACrB,SAAS;AAAA,QACT,mBAAmB,KAAK,KAAK,WAAW,GAAI;AAAA,MAC9C,CAAC;AAAA,IACH;AACA,WAAO,SAAS;AAChB,UAAM,oBAAoB,KAAK,MAAM,OAAO,UAAU,OAAO,GAAI;AACjE,QAAI,OAAO,QAAQ,OAAO;AACxB,aAAO,QAAQ,QAAQ,EAAE,SAAS,MAAM,kBAAkB,CAAC;AAAA,IAC7D;AACA,WAAO,QAAQ,QAAQ,EAAE,SAAS,OAAO,kBAAkB,CAAC;AAAA,EAC9D;AACF;;;AC/DA,IAAI,cAA2C;AAExC,SAAS,eAAe,SAA4C;AACzE,gBAAc;AAChB;AAEO,SAAS,iBAAuC;AACrD,MAAI,CAAC,aAAa;AAChB,kBAAc,IAAI,oBAAoB;AAAA,EACxC;AACA,SAAO;AACT;AAEO,SAAS,yBAAsD;AACpE,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getScopedLogger
|
|
3
|
+
} from "./chunk-JJL74ZPK.js";
|
|
4
|
+
|
|
5
|
+
// src/observability/safety-check.ts
|
|
6
|
+
var MIN_PROD_SECRET_LENGTH = 32;
|
|
7
|
+
function verifyStartupSafety(input) {
|
|
8
|
+
const log = getScopedLogger({ subsystem: "boot" });
|
|
9
|
+
const emitted = [];
|
|
10
|
+
const multiNode = input.multiNodeFlag === "true" || input.multiNodeFlag === "1";
|
|
11
|
+
const explicitOptOut = input.multiNodeFlag === "false" || input.multiNodeFlag === "0";
|
|
12
|
+
const containerInProd = !explicitOptOut && input.nodeEnv === "production" && Boolean(input.containerEnv);
|
|
13
|
+
const likelyMultiNode = multiNode || containerInProd;
|
|
14
|
+
if (likelyMultiNode && input.storageAdapter === "local") {
|
|
15
|
+
const reason = multiNode ? "explicit_flag" : "container_hint";
|
|
16
|
+
const trigger = multiNode ? "NP_MULTI_NODE is set" : "a managed-container env var was detected in production (KUBERNETES_SERVICE_HOST / FLY_REGION / RENDER_INSTANCE_ID / RAILWAY_ENVIRONMENT_NAME)";
|
|
17
|
+
log.warn(
|
|
18
|
+
`LocalStorageAdapter is not multi-node safe \u2014 ${trigger} but ./uploads is per-process. Set NP_STORAGE_ADAPTER=s3 (or NP_MULTI_NODE=false to silence the hint on a single-node deploy).`,
|
|
19
|
+
{ check: "multi_node_local_storage", reason }
|
|
20
|
+
);
|
|
21
|
+
emitted.push("multi_node_local_storage");
|
|
22
|
+
}
|
|
23
|
+
if (likelyMultiNode && input.rateLimiterCustom === false) {
|
|
24
|
+
const reason = multiNode ? "explicit_flag" : "container_hint";
|
|
25
|
+
log.warn(
|
|
26
|
+
"InMemoryRateLimiter is not multi-node safe \u2014 buckets are per-process, so a multi-replica deploy multiplies the effective limit by the replica count. Install a shared adapter via `setRateLimiter(new RedisRateLimiter(...))` (or your own backing store), or `NP_MULTI_NODE=false` to silence this on a single-node deploy.",
|
|
27
|
+
{ check: "multi_node_in_memory_rate_limiter", reason }
|
|
28
|
+
);
|
|
29
|
+
emitted.push("multi_node_in_memory_rate_limiter");
|
|
30
|
+
}
|
|
31
|
+
if (input.nodeEnv === "production") {
|
|
32
|
+
if (!input.secret) {
|
|
33
|
+
log.warn(
|
|
34
|
+
"NP_SECRET is unset in production \u2014 JWT sessions are signed with an empty key, which is forgeable.",
|
|
35
|
+
{ check: "missing_prod_secret" }
|
|
36
|
+
);
|
|
37
|
+
emitted.push("missing_prod_secret");
|
|
38
|
+
} else if (input.secret.length < MIN_PROD_SECRET_LENGTH) {
|
|
39
|
+
log.warn(
|
|
40
|
+
`NP_SECRET is shorter than ${MIN_PROD_SECRET_LENGTH} characters in production \u2014 pick a longer secret to avoid weak-key attacks.`,
|
|
41
|
+
{ check: "weak_prod_secret", length: input.secret.length }
|
|
42
|
+
);
|
|
43
|
+
emitted.push("weak_prod_secret");
|
|
44
|
+
}
|
|
45
|
+
if (input.emailAdapterEnv === void 0 ? false : input.emailAdapterEnv === null || input.emailAdapterEnv === "noop") {
|
|
46
|
+
log.warn(
|
|
47
|
+
"NP_EMAIL_ADAPTER is unset (or `noop`) in production \u2014 transactional mail (password reset, email verify, member digests) is silently dropped. Set NP_EMAIL_ADAPTER=smtp + the NP_SMTP_* config, or install a custom adapter via setEmailAdapter() in your bootstrap code (in which case this warning is a false positive \u2014 safe to ignore).",
|
|
48
|
+
{ check: "noop_email_in_prod" }
|
|
49
|
+
);
|
|
50
|
+
emitted.push("noop_email_in_prod");
|
|
51
|
+
}
|
|
52
|
+
if (input.databaseHost && isLoopbackHost(input.databaseHost)) {
|
|
53
|
+
log.warn(
|
|
54
|
+
`DATABASE_URL host is "${input.databaseHost}" in production \u2014 that's loopback, almost certainly a stale dev connection string. Point DATABASE_URL at the production Postgres instance.`,
|
|
55
|
+
{ check: "loopback_database_in_prod", host: input.databaseHost }
|
|
56
|
+
);
|
|
57
|
+
emitted.push("loopback_database_in_prod");
|
|
58
|
+
}
|
|
59
|
+
if (input.siteUrl === null) {
|
|
60
|
+
log.warn(
|
|
61
|
+
"SITE_URL is unset in production \u2014 sitemap URLs, SEO canonical URLs, OAuth callbacks, and email links anchor on it. Set SITE_URL to your public origin (e.g. https://example.com).",
|
|
62
|
+
{ check: "missing_site_url" }
|
|
63
|
+
);
|
|
64
|
+
emitted.push("missing_site_url");
|
|
65
|
+
} else if (typeof input.siteUrl === "string" && isLoopbackUrl(input.siteUrl)) {
|
|
66
|
+
log.warn(
|
|
67
|
+
`SITE_URL is "${input.siteUrl}" in production \u2014 loopback origins break share links, OAuth round-trips, and outbound email links. Set SITE_URL to your public origin.`,
|
|
68
|
+
{ check: "loopback_site_url", siteUrl: input.siteUrl }
|
|
69
|
+
);
|
|
70
|
+
emitted.push("loopback_site_url");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return emitted;
|
|
74
|
+
}
|
|
75
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
76
|
+
function isLoopbackHost(host) {
|
|
77
|
+
return LOOPBACK_HOSTS.has(host.toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
function isLoopbackUrl(url) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = new URL(url);
|
|
82
|
+
const host = parsed.hostname.replace(/^\[/, "").replace(/\]$/, "");
|
|
83
|
+
return isLoopbackHost(host);
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
verifyStartupSafety
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=chunk-2YDGE7YX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/observability/safety-check.ts"],"sourcesContent":["import { getScopedLogger } from \"./logger.js\";\n\n/**\n * Inputs to {@link verifyStartupSafety}. The bootstrap layer\n * (`packages/next/src/bootstrap.ts`) reads the resolved config and the\n * relevant env vars and hands them in — keeping this helper a pure\n * function of its input means it stays trivially testable and never\n * accidentally reads `process.env` from a deeper code path.\n */\nexport interface NpStartupSafetyInput {\n /** Storage adapter id chosen by `createStorageAdapter` (`local` or `s3`). */\n storageAdapter: \"local\" | \"s3\";\n /** Resolved auth secret. `null` when unset. */\n secret: string | null;\n /** `process.env.NODE_ENV` at boot — typically `\"production\"` / `\"development\"` / undefined. */\n nodeEnv: string | undefined;\n /**\n * `process.env.NP_MULTI_NODE` at boot. When the operator opts into\n * multi-node mode we tighten checks that are otherwise just hints.\n */\n multiNodeFlag: string | undefined;\n /**\n * True when the boot environment looks like a managed container\n * runtime (Kubernetes / Fly.io / Render / similar). The bootstrap\n * layer evaluates the well-known env vars and hands a single bool\n * in so this helper stays a pure function. We use this together\n * with `nodeEnv === \"production\"` to catch the common footgun\n * where an operator deploys to a multi-replica platform and forgot\n * to set `NP_MULTI_NODE=true`. Optional for back-compat with\n * callers that don't supply it (treated as `false`).\n */\n containerEnv?: boolean;\n /**\n * Value of `NP_EMAIL_ADAPTER` at boot. Pass `null` when the env\n * var is unset, the literal string when it is — we check the\n * **operator's intent** rather than the live adapter because the\n * adapter is configured later in the boot sequence (after the\n * core-services step that runs this safety check). Use `null` /\n * `\"noop\"` to mean \"operator hasn't asked for a real adapter,\n * warn in production.\" Custom adapters wired via\n * `setEmailAdapter()` programmatically will surface a false\n * positive here — the warning text calls that out.\n */\n emailAdapterEnv?: string | null;\n /**\n * Hostname extracted from `DATABASE_URL`. Optional for back-compat.\n * When provided and the host looks like loopback (`localhost` /\n * `127.0.0.1` / `::1`) AND `nodeEnv === \"production\"`, we warn —\n * the operator likely shipped the dev DB connection string.\n */\n databaseHost?: string | null;\n /**\n * Resolved `SITE_URL` (or equivalent base URL) at boot. Optional\n * for back-compat. When unset OR loopback-shaped in production,\n * we warn — links, SEO canonical URLs, OAuth callbacks, sitemap\n * URLs all anchor on this value.\n */\n siteUrl?: string | null;\n /**\n * Whether the operator has opted into a custom rate-limiter\n * adapter via `setRateLimiter(...)`. `false` means the default\n * `InMemoryRateLimiter` will be lazily installed on first use —\n * fine for single-node, but per-node buckets in multi-replica\n * deploys make the limit ~Nx looser than configured. Optional\n * for back-compat; `undefined` skips the check.\n */\n rateLimiterCustom?: boolean;\n}\n\nconst MIN_PROD_SECRET_LENGTH = 32;\n\n/**\n * Surfaces operationally-bitten misconfiguration as warnings during\n * boot. The set is intentionally small — every entry has to map to\n * a real failure mode that has either bitten the project or been\n * called out in the deployment docs:\n *\n * - `LocalStorageAdapter` + `NP_MULTI_NODE=true`. Different nodes\n * see different `./uploads` directories; uploads disappear\n * between requests. (`docs/deployment.md` — Multi-node notes.)\n * - `LocalStorageAdapter` + `NODE_ENV=production` + a managed-\n * container env var (`KUBERNETES_SERVICE_HOST`, `FLY_REGION`,\n * `RENDER_INSTANCE_ID`, `RAILWAY_ENVIRONMENT_NAME`, …). Same\n * failure mode as above; this branch catches the operator who\n * forgot to set `NP_MULTI_NODE` but is clearly running on a\n * multi-replica platform.\n * - `NODE_ENV=production` + missing or short `NP_SECRET`. Tokens\n * signed with a weak secret are forgeable. We cap below 32 bytes\n * because that's the floor `signJwt` documents.\n * - `NODE_ENV=production` + `emailAdapterKind === \"noop\"` (#597).\n * Transactional mail (password reset, email verify, member\n * digests) silently dropped — operators expect those to deliver.\n * - `NODE_ENV=production` + `databaseHost` looks like loopback\n * (#597). `localhost` / `127.0.0.1` / `::1` in a prod deploy is\n * almost always a stale dev `DATABASE_URL` that slipped through.\n * - `NODE_ENV=production` + `siteUrl` unset or loopback-shaped\n * (#597). Sitemap URLs, SEO canonical URLs, OAuth callbacks,\n * transactional mail links all anchor on this — wrong value\n * manifests as broken share links and unverifiable OAuth round-\n * trips, which take a while for the operator to notice.\n *\n * Warnings go through {@link getScopedLogger} so production deploys\n * that have called `setLogger(...)` get them in their structured-log\n * pipeline (Datadog, Axiom, etc.) instead of stdout. Returns the list\n * of emitted warning ids so callers can assert on them in tests; in\n * production nothing inspects the return value.\n */\nexport function verifyStartupSafety(input: NpStartupSafetyInput): readonly string[] {\n const log = getScopedLogger({ subsystem: \"boot\" });\n const emitted: string[] = [];\n\n const multiNode = input.multiNodeFlag === \"true\" || input.multiNodeFlag === \"1\";\n const explicitOptOut = input.multiNodeFlag === \"false\" || input.multiNodeFlag === \"0\";\n // Explicit opt-out wins over the container heuristic: an\n // operator who deliberately sets `NP_MULTI_NODE=false` on a\n // managed-container deploy (single-replica on Kubernetes, etc.)\n // should not see the hint, otherwise the warning the message\n // tells them to silence isn't actually silenceable.\n const containerInProd =\n !explicitOptOut && input.nodeEnv === \"production\" && Boolean(input.containerEnv);\n const likelyMultiNode = multiNode || containerInProd;\n\n if (likelyMultiNode && input.storageAdapter === \"local\") {\n const reason = multiNode ? \"explicit_flag\" : \"container_hint\";\n const trigger = multiNode\n ? \"NP_MULTI_NODE is set\"\n : \"a managed-container env var was detected in production (KUBERNETES_SERVICE_HOST / FLY_REGION / RENDER_INSTANCE_ID / RAILWAY_ENVIRONMENT_NAME)\";\n log.warn(\n `LocalStorageAdapter is not multi-node safe — ${trigger} but ./uploads is per-process. ` +\n \"Set NP_STORAGE_ADAPTER=s3 (or NP_MULTI_NODE=false to silence the hint on a single-node deploy).\",\n { check: \"multi_node_local_storage\", reason },\n );\n emitted.push(\"multi_node_local_storage\");\n }\n\n // The default in-memory rate limiter is per-process. On a\n // multi-replica deploy each pod tracks its own buckets, so a\n // configured \"5 login attempts / minute\" effectively becomes\n // \"5 × N pods\" — the gate is looser than the operator thinks.\n // Same likely-multi-node detection as storage. `rateLimiterCustom\n // === false` means the operator hasn't called `setRateLimiter()`,\n // so the default will be installed on first request. `undefined`\n // (caller didn't supply) skips the check.\n if (likelyMultiNode && input.rateLimiterCustom === false) {\n const reason = multiNode ? \"explicit_flag\" : \"container_hint\";\n log.warn(\n \"InMemoryRateLimiter is not multi-node safe — buckets are per-process, so a multi-replica deploy multiplies the effective limit by the replica count. \" +\n \"Install a shared adapter via `setRateLimiter(new RedisRateLimiter(...))` (or your own backing store), or `NP_MULTI_NODE=false` to silence this on a single-node deploy.\",\n { check: \"multi_node_in_memory_rate_limiter\", reason },\n );\n emitted.push(\"multi_node_in_memory_rate_limiter\");\n }\n\n if (input.nodeEnv === \"production\") {\n if (!input.secret) {\n log.warn(\n \"NP_SECRET is unset in production — JWT sessions are signed with an empty key, which is forgeable.\",\n { check: \"missing_prod_secret\" },\n );\n emitted.push(\"missing_prod_secret\");\n } else if (input.secret.length < MIN_PROD_SECRET_LENGTH) {\n log.warn(\n `NP_SECRET is shorter than ${MIN_PROD_SECRET_LENGTH} characters in production — pick a longer secret to avoid weak-key attacks.`,\n { check: \"weak_prod_secret\", length: input.secret.length },\n );\n emitted.push(\"weak_prod_secret\");\n }\n\n // Email adapter intent comes from `NP_EMAIL_ADAPTER` (env-driven\n // path, the typical setup). Unset / \"noop\" → operator hasn't\n // asked for a real adapter; transactional mail (password reset,\n // email verify, member digests) silently disappears. We check\n // the env var rather than the live adapter because adapters get\n // wired AFTER this safety check runs in the boot sequence — a\n // live-adapter check would always see `noop`. False-positive:\n // operators who skip the env var and call `setEmailAdapter()`\n // programmatically with a custom adapter (Resend / SendGrid /\n // etc.) will see this warning despite being correctly\n // configured. The warning text below calls that out so they can\n // ignore it.\n if (\n input.emailAdapterEnv === undefined\n ? false // back-compat: caller didn't supply\n : input.emailAdapterEnv === null || input.emailAdapterEnv === \"noop\"\n ) {\n log.warn(\n \"NP_EMAIL_ADAPTER is unset (or `noop`) in production — transactional mail (password reset, email verify, member digests) is silently dropped. \" +\n \"Set NP_EMAIL_ADAPTER=smtp + the NP_SMTP_* config, or install a custom adapter via setEmailAdapter() in your bootstrap code (in which case this warning is a false positive — safe to ignore).\",\n { check: \"noop_email_in_prod\" },\n );\n emitted.push(\"noop_email_in_prod\");\n }\n\n if (input.databaseHost && isLoopbackHost(input.databaseHost)) {\n log.warn(\n `DATABASE_URL host is \"${input.databaseHost}\" in production — that's loopback, almost certainly a stale dev connection string. ` +\n \"Point DATABASE_URL at the production Postgres instance.\",\n { check: \"loopback_database_in_prod\", host: input.databaseHost },\n );\n emitted.push(\"loopback_database_in_prod\");\n }\n\n // siteUrl undefined === \"caller (older bootstrap) didn't supply\n // info, skip the check\"; siteUrl === null === \"caller checked and\n // confirmed it's unset\". Only the explicit-null case warns. This\n // back-compat shape lets `verifyStartupSafety` evolve its input\n // surface without breaking existing call sites whose tests didn't\n // know to set the new fields.\n if (input.siteUrl === null) {\n log.warn(\n \"SITE_URL is unset in production — sitemap URLs, SEO canonical URLs, OAuth callbacks, and email links anchor on it. \" +\n \"Set SITE_URL to your public origin (e.g. https://example.com).\",\n { check: \"missing_site_url\" },\n );\n emitted.push(\"missing_site_url\");\n } else if (typeof input.siteUrl === \"string\" && isLoopbackUrl(input.siteUrl)) {\n log.warn(\n `SITE_URL is \"${input.siteUrl}\" in production — loopback origins break share links, OAuth round-trips, and outbound email links. ` +\n \"Set SITE_URL to your public origin.\",\n { check: \"loopback_site_url\", siteUrl: input.siteUrl },\n );\n emitted.push(\"loopback_site_url\");\n }\n }\n\n return emitted;\n}\n\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"0.0.0.0\"]);\n\nfunction isLoopbackHost(host: string): boolean {\n return LOOPBACK_HOSTS.has(host.toLowerCase());\n}\n\nfunction isLoopbackUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n // IPv6 hostnames keep their square brackets in `URL.hostname`\n // (e.g. `new URL(\"http://[::1]/\").hostname === \"[::1]\"`). Strip\n // them so the comparison hits our canonical loopback set.\n const host = parsed.hostname.replace(/^\\[/, \"\").replace(/\\]$/, \"\");\n return isLoopbackHost(host);\n } catch {\n // Malformed URL — treat as not-loopback so we don't double-warn.\n // The caller's own URL parser will surface the malformation.\n return false;\n }\n}\n"],"mappings":";;;;;AAqEA,IAAM,yBAAyB;AAsCxB,SAAS,oBAAoB,OAAgD;AAClF,QAAM,MAAM,gBAAgB,EAAE,WAAW,OAAO,CAAC;AACjD,QAAM,UAAoB,CAAC;AAE3B,QAAM,YAAY,MAAM,kBAAkB,UAAU,MAAM,kBAAkB;AAC5E,QAAM,iBAAiB,MAAM,kBAAkB,WAAW,MAAM,kBAAkB;AAMlF,QAAM,kBACJ,CAAC,kBAAkB,MAAM,YAAY,gBAAgB,QAAQ,MAAM,YAAY;AACjF,QAAM,kBAAkB,aAAa;AAErC,MAAI,mBAAmB,MAAM,mBAAmB,SAAS;AACvD,UAAM,SAAS,YAAY,kBAAkB;AAC7C,UAAM,UAAU,YACZ,yBACA;AACJ,QAAI;AAAA,MACF,qDAAgD,OAAO;AAAA,MAEvD,EAAE,OAAO,4BAA4B,OAAO;AAAA,IAC9C;AACA,YAAQ,KAAK,0BAA0B;AAAA,EACzC;AAUA,MAAI,mBAAmB,MAAM,sBAAsB,OAAO;AACxD,UAAM,SAAS,YAAY,kBAAkB;AAC7C,QAAI;AAAA,MACF;AAAA,MAEA,EAAE,OAAO,qCAAqC,OAAO;AAAA,IACvD;AACA,YAAQ,KAAK,mCAAmC;AAAA,EAClD;AAEA,MAAI,MAAM,YAAY,cAAc;AAClC,QAAI,CAAC,MAAM,QAAQ;AACjB,UAAI;AAAA,QACF;AAAA,QACA,EAAE,OAAO,sBAAsB;AAAA,MACjC;AACA,cAAQ,KAAK,qBAAqB;AAAA,IACpC,WAAW,MAAM,OAAO,SAAS,wBAAwB;AACvD,UAAI;AAAA,QACF,6BAA6B,sBAAsB;AAAA,QACnD,EAAE,OAAO,oBAAoB,QAAQ,MAAM,OAAO,OAAO;AAAA,MAC3D;AACA,cAAQ,KAAK,kBAAkB;AAAA,IACjC;AAcA,QACE,MAAM,oBAAoB,SACtB,QACA,MAAM,oBAAoB,QAAQ,MAAM,oBAAoB,QAChE;AACA,UAAI;AAAA,QACF;AAAA,QAEA,EAAE,OAAO,qBAAqB;AAAA,MAChC;AACA,cAAQ,KAAK,oBAAoB;AAAA,IACnC;AAEA,QAAI,MAAM,gBAAgB,eAAe,MAAM,YAAY,GAAG;AAC5D,UAAI;AAAA,QACF,yBAAyB,MAAM,YAAY;AAAA,QAE3C,EAAE,OAAO,6BAA6B,MAAM,MAAM,aAAa;AAAA,MACjE;AACA,cAAQ,KAAK,2BAA2B;AAAA,IAC1C;AAQA,QAAI,MAAM,YAAY,MAAM;AAC1B,UAAI;AAAA,QACF;AAAA,QAEA,EAAE,OAAO,mBAAmB;AAAA,MAC9B;AACA,cAAQ,KAAK,kBAAkB;AAAA,IACjC,WAAW,OAAO,MAAM,YAAY,YAAY,cAAc,MAAM,OAAO,GAAG;AAC5E,UAAI;AAAA,QACF,gBAAgB,MAAM,OAAO;AAAA,QAE7B,EAAE,OAAO,qBAAqB,SAAS,MAAM,QAAQ;AAAA,MACvD;AACA,cAAQ,KAAK,mBAAmB;AAAA,IAClC;AAAA,EACF;AAEA,SAAO;AACT;AAEA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,SAAS,CAAC;AAE3E,SAAS,eAAe,MAAuB;AAC7C,SAAO,eAAe,IAAI,KAAK,YAAY,CAAC;AAC9C;AAEA,SAAS,cAAc,KAAsB;AAC3C,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAI1B,UAAM,OAAO,OAAO,SAAS,QAAQ,OAAO,EAAE,EAAE,QAAQ,OAAO,EAAE;AACjE,WAAO,eAAe,IAAI;AAAA,EAC5B,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;","names":[]}
|