@strapi/admin 5.57.0 → 5.58.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/dist/admin/admin/src/components/FormInputs/Boolean.js +3 -2
- package/dist/admin/admin/src/components/FormInputs/Boolean.js.map +1 -1
- package/dist/admin/admin/src/components/FormInputs/Boolean.mjs +3 -2
- package/dist/admin/admin/src/components/FormInputs/Boolean.mjs.map +1 -1
- package/dist/admin/admin/src/constants.js +14 -0
- package/dist/admin/admin/src/constants.js.map +1 -1
- package/dist/admin/admin/src/constants.mjs +14 -0
- package/dist/admin/admin/src/constants.mjs.map +1 -1
- package/dist/admin/admin/src/pages/Auth/components/ResetPassword.js +6 -12
- package/dist/admin/admin/src/pages/Auth/components/ResetPassword.js.map +1 -1
- package/dist/admin/admin/src/pages/Auth/components/ResetPassword.mjs +6 -12
- package/dist/admin/admin/src/pages/Auth/components/ResetPassword.mjs.map +1 -1
- package/dist/admin/admin/src/pages/Settings/components/SettingsNav.js +10 -2
- package/dist/admin/admin/src/pages/Settings/components/SettingsNav.js.map +1 -1
- package/dist/admin/admin/src/pages/Settings/components/SettingsNav.mjs +10 -2
- package/dist/admin/admin/src/pages/Settings/components/SettingsNav.mjs.map +1 -1
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/ApplicationInfoPage.js +6 -56
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/ApplicationInfoPage.js.map +1 -1
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/ApplicationInfoPage.mjs +6 -56
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/ApplicationInfoPage.mjs.map +1 -1
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/DiagnosticSnapshotModal.js +217 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/DiagnosticSnapshotModal.js.map +1 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/DiagnosticSnapshotModal.mjs +195 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/DiagnosticSnapshotModal.mjs.map +1 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/PlanCard.js +185 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/PlanCard.js.map +1 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/PlanCard.mjs +162 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/PlanCard.mjs.map +1 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/SupportCard.js +342 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/SupportCard.js.map +1 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/SupportCard.mjs +320 -0
- package/dist/admin/admin/src/pages/Settings/pages/ApplicationInfo/components/SupportCard.mjs.map +1 -0
- package/dist/admin/admin/src/services/admin.js +11 -1
- package/dist/admin/admin/src/services/admin.js.map +1 -1
- package/dist/admin/admin/src/services/admin.mjs +11 -2
- package/dist/admin/admin/src/services/admin.mjs.map +1 -1
- package/dist/admin/admin/src/translations/en.json.js +71 -4
- package/dist/admin/admin/src/translations/en.json.js.map +1 -1
- package/dist/admin/admin/src/translations/en.json.mjs +71 -4
- package/dist/admin/admin/src/translations/en.json.mjs.map +1 -1
- package/dist/admin/admin/src/utils/browserStrapi.js +1 -1
- package/dist/admin/admin/src/utils/browserStrapi.js.map +1 -1
- package/dist/admin/admin/src/utils/browserStrapi.mjs +1 -1
- package/dist/admin/admin/src/utils/browserStrapi.mjs.map +1 -1
- package/dist/admin/admin/src/utils/getFetchClient.js +24 -0
- package/dist/admin/admin/src/utils/getFetchClient.js.map +1 -1
- package/dist/admin/admin/src/utils/getFetchClient.mjs +24 -1
- package/dist/admin/admin/src/utils/getFetchClient.mjs.map +1 -1
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AIUsage.js +52 -22
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AIUsage.js.map +1 -1
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AIUsage.mjs +53 -23
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AIUsage.mjs.map +1 -1
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AdminSeatInfo.js +16 -29
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AdminSeatInfo.js.map +1 -1
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AdminSeatInfo.mjs +18 -31
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AdminSeatInfo.mjs.map +1 -1
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/LicenseInfo.js +473 -0
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/LicenseInfo.js.map +1 -0
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/LicenseInfo.mjs +471 -0
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/LicenseInfo.mjs.map +1 -0
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/utils/getActionTypesDefaultMessages.js +2 -0
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/utils/getActionTypesDefaultMessages.js.map +1 -1
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/utils/getActionTypesDefaultMessages.mjs +2 -0
- package/dist/admin/ee/admin/src/pages/SettingsPage/pages/AuditLogs/utils/getActionTypesDefaultMessages.mjs.map +1 -1
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/shared/utils/get-project-type.js +49 -0
- package/dist/admin/shared/utils/get-project-type.js.map +1 -0
- package/dist/admin/shared/utils/get-project-type.mjs +46 -0
- package/dist/admin/shared/utils/get-project-type.mjs.map +1 -0
- package/dist/admin/src/constants.d.ts +10 -0
- package/dist/admin/src/pages/Settings/pages/ApplicationInfo/ApplicationInfoPage.d.ts +1 -1
- package/dist/admin/src/pages/Settings/pages/ApplicationInfo/components/DiagnosticSnapshotModal.d.ts +12 -0
- package/dist/admin/src/pages/Settings/pages/ApplicationInfo/components/PlanCard.d.ts +14 -0
- package/dist/admin/src/pages/Settings/pages/ApplicationInfo/components/SupportCard.d.ts +2 -0
- package/dist/admin/src/services/admin.d.ts +2 -2
- package/dist/admin/src/types/permissions.d.ts +1 -0
- package/dist/admin/src/utils/getFetchClient.d.ts +25 -0
- package/dist/ee/admin/src/hooks/useLicenseLimits.d.ts +29 -0
- package/dist/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/AIUsage.d.ts +6 -1
- package/dist/ee/admin/src/pages/SettingsPage/pages/ApplicationInfoPage/components/LicenseInfo.d.ts +2 -0
- package/dist/ee/admin/src/pages/SettingsPage/pages/AuditLogs/utils/getActionTypesDefaultMessages.d.ts +2 -0
- package/dist/ee/server/src/audit-logs/entitlements.d.ts +8 -0
- package/dist/ee/server/src/audit-logs/entitlements.d.ts.map +1 -0
- package/dist/ee/server/src/audit-logs/services/lifecycles.d.ts +16 -0
- package/dist/ee/server/src/audit-logs/services/lifecycles.d.ts.map +1 -1
- package/dist/ee/server/src/controllers/admin.d.ts +36 -10
- package/dist/ee/server/src/controllers/admin.d.ts.map +1 -1
- package/dist/ee/server/src/controllers/authentication-utils/middlewares.d.ts.map +1 -1
- package/dist/ee/server/src/controllers/index.d.ts +36 -10
- package/dist/ee/server/src/controllers/index.d.ts.map +1 -1
- package/dist/ee/server/src/index.d.ts +36 -10
- package/dist/ee/server/src/index.d.ts.map +1 -1
- package/dist/server/ee/server/src/audit-logs/entitlements.js +25 -0
- package/dist/server/ee/server/src/audit-logs/entitlements.js.map +1 -0
- package/dist/server/ee/server/src/audit-logs/entitlements.mjs +23 -0
- package/dist/server/ee/server/src/audit-logs/entitlements.mjs.map +1 -0
- package/dist/server/ee/server/src/audit-logs/services/lifecycles.js +20 -8
- package/dist/server/ee/server/src/audit-logs/services/lifecycles.js.map +1 -1
- package/dist/server/ee/server/src/audit-logs/services/lifecycles.mjs +19 -9
- package/dist/server/ee/server/src/audit-logs/services/lifecycles.mjs.map +1 -1
- package/dist/server/ee/server/src/controllers/admin.js +76 -3
- package/dist/server/ee/server/src/controllers/admin.js.map +1 -1
- package/dist/server/ee/server/src/controllers/admin.mjs +76 -3
- package/dist/server/ee/server/src/controllers/admin.mjs.map +1 -1
- package/dist/server/ee/server/src/controllers/authentication-utils/middlewares.js +28 -10
- package/dist/server/ee/server/src/controllers/authentication-utils/middlewares.js.map +1 -1
- package/dist/server/ee/server/src/controllers/authentication-utils/middlewares.mjs +28 -10
- package/dist/server/ee/server/src/controllers/authentication-utils/middlewares.mjs.map +1 -1
- package/dist/server/ee/server/src/index.js +2 -0
- package/dist/server/ee/server/src/index.js.map +1 -1
- package/dist/server/ee/server/src/index.mjs +2 -0
- package/dist/server/ee/server/src/index.mjs.map +1 -1
- package/dist/server/server/src/audit-logs/auth.js +72 -0
- package/dist/server/server/src/audit-logs/auth.js.map +1 -0
- package/dist/server/server/src/audit-logs/auth.mjs +67 -0
- package/dist/server/server/src/audit-logs/auth.mjs.map +1 -0
- package/dist/server/server/src/config/admin-actions.js +7 -0
- package/dist/server/server/src/config/admin-actions.js.map +1 -1
- package/dist/server/server/src/config/admin-actions.mjs +7 -0
- package/dist/server/server/src/config/admin-actions.mjs.map +1 -1
- package/dist/server/server/src/controllers/admin.js +6 -0
- package/dist/server/server/src/controllers/admin.js.map +1 -1
- package/dist/server/server/src/controllers/admin.mjs +6 -0
- package/dist/server/server/src/controllers/admin.mjs.map +1 -1
- package/dist/server/server/src/controllers/authentication.js +34 -8
- package/dist/server/server/src/controllers/authentication.js.map +1 -1
- package/dist/server/server/src/controllers/authentication.mjs +34 -8
- package/dist/server/server/src/controllers/authentication.mjs.map +1 -1
- package/dist/server/server/src/register.js +4 -0
- package/dist/server/server/src/register.js.map +1 -1
- package/dist/server/server/src/register.mjs +4 -0
- package/dist/server/server/src/register.mjs.map +1 -1
- package/dist/server/server/src/routes/admin.js +18 -0
- package/dist/server/server/src/routes/admin.js.map +1 -1
- package/dist/server/server/src/routes/admin.mjs +18 -0
- package/dist/server/server/src/routes/admin.mjs.map +1 -1
- package/dist/server/server/src/services/auth.js +4 -2
- package/dist/server/server/src/services/auth.js.map +1 -1
- package/dist/server/server/src/services/auth.mjs +4 -3
- package/dist/server/server/src/services/auth.mjs.map +1 -1
- package/dist/server/server/src/services/debug-dump.js +173 -0
- package/dist/server/server/src/services/debug-dump.js.map +1 -0
- package/dist/server/server/src/services/debug-dump.mjs +166 -0
- package/dist/server/server/src/services/debug-dump.mjs.map +1 -0
- package/dist/server/server/src/services/index.js +3 -1
- package/dist/server/server/src/services/index.js.map +1 -1
- package/dist/server/server/src/services/index.mjs +3 -1
- package/dist/server/server/src/services/index.mjs.map +1 -1
- package/dist/server/server/src/utils/debug-dump/redact.js +197 -0
- package/dist/server/server/src/utils/debug-dump/redact.js.map +1 -0
- package/dist/server/server/src/utils/debug-dump/redact.mjs +194 -0
- package/dist/server/server/src/utils/debug-dump/redact.mjs.map +1 -0
- package/dist/server/shared/utils/get-project-type.js +49 -0
- package/dist/server/shared/utils/get-project-type.js.map +1 -0
- package/dist/server/shared/utils/get-project-type.mjs +46 -0
- package/dist/server/shared/utils/get-project-type.mjs.map +1 -0
- package/dist/server/src/audit-logs/auth.d.ts +72 -0
- package/dist/server/src/audit-logs/auth.d.ts.map +1 -0
- package/dist/server/src/config/admin-actions.d.ts.map +1 -1
- package/dist/server/src/controllers/admin.d.ts +3 -0
- package/dist/server/src/controllers/admin.d.ts.map +1 -1
- package/dist/server/src/controllers/authentication.d.ts.map +1 -1
- package/dist/server/src/controllers/index.d.ts +3 -0
- package/dist/server/src/controllers/index.d.ts.map +1 -1
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/register.d.ts.map +1 -1
- package/dist/server/src/routes/admin.d.ts.map +1 -1
- package/dist/server/src/services/auth.d.ts +6 -0
- package/dist/server/src/services/auth.d.ts.map +1 -1
- package/dist/server/src/services/debug-dump.d.ts +9 -0
- package/dist/server/src/services/debug-dump.d.ts.map +1 -0
- package/dist/server/src/services/index.d.ts +5 -0
- package/dist/server/src/services/index.d.ts.map +1 -1
- package/dist/server/src/utils/debug-dump/redact.d.ts +8 -0
- package/dist/server/src/utils/debug-dump/redact.d.ts.map +1 -0
- package/dist/shared/contracts/admin.d.ts +106 -0
- package/dist/shared/contracts/admin.d.ts.map +1 -1
- package/dist/shared/utils/__tests__/get-project-type.test.d.ts +1 -0
- package/dist/shared/utils/__tests__/get-project-type.test.d.ts.map +1 -0
- package/dist/shared/utils/get-project-type.d.ts +37 -0
- package/dist/shared/utils/get-project-type.d.ts.map +1 -0
- package/package.json +8 -8
- package/dist/admin/admin/src/utils/getProjectType.js +0 -22
- package/dist/admin/admin/src/utils/getProjectType.js.map +0 -1
- package/dist/admin/admin/src/utils/getProjectType.mjs +0 -20
- package/dist/admin/admin/src/utils/getProjectType.mjs.map +0 -1
- package/dist/admin/src/utils/getProjectType.d.ts +0 -16
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const REDACTED = '[REDACTED]';
|
|
4
|
+
// Layer 1a: key names (substring match) whose value must be masked wherever they appear.
|
|
5
|
+
const SECRET_KEY_PATTERN = /(secret|password|passphrase|salt|token|api[_-]?key|access[_-]?key|license[_-]?key|private[_-]?key|encryption[_-]?key|credential|provider[_-]?options|connection[_-]?string|mnemonic|seed[_-]?phrase|authorization|bearer)/i;
|
|
6
|
+
// Layer 1b: short, ambiguous key names matched EXACTLY (case-insensitive), so they
|
|
7
|
+
// do not over-match as substrings (e.g. `pass` must not redact `compass`/`bypass`,
|
|
8
|
+
// and this still catches a nested SMTP `auth.pass`).
|
|
9
|
+
const SECRET_KEY_EXACT = new Set([
|
|
10
|
+
'pass',
|
|
11
|
+
'pwd',
|
|
12
|
+
'passwd',
|
|
13
|
+
'dsn'
|
|
14
|
+
]);
|
|
15
|
+
// camelCase password suffixes (capitalized), e.g. `smtpAuthPass`, `dbPwd` — the
|
|
16
|
+
// capital letter distinguishes them from lowercase look-alikes (`compass`, `bypass`).
|
|
17
|
+
const SECRET_KEY_SUFFIX = /[a-z](Pass|Pwd|Passwd)$/;
|
|
18
|
+
// separator-delimited short secret names, e.g. `auth.pass`, `db_pwd`, `x-pass`.
|
|
19
|
+
const SECRET_KEY_DELIMITED = /(^|[._-])(pass|pwd|passwd|dsn)([._-]|$)/i;
|
|
20
|
+
const isSecretKey = (key)=>SECRET_KEY_PATTERN.test(key) || SECRET_KEY_EXACT.has(key.toLowerCase()) || SECRET_KEY_SUFFIX.test(key) || SECRET_KEY_DELIMITED.test(key);
|
|
21
|
+
// Layer 3: string values that look like secrets regardless of their key.
|
|
22
|
+
// Real JWTs are base64url-encoded and the header segment always begins with
|
|
23
|
+
// `eyJ` (the base64url encoding of `{"`), so requiring that prefix avoids
|
|
24
|
+
// matching arbitrary three-part dotted strings like semver versions
|
|
25
|
+
// (e.g. "5.50.1", "18.3.1").
|
|
26
|
+
const JWT_PATTERN = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
27
|
+
const LONG_TOKEN_PATTERN = /^[A-Fa-f0-9]{32,}$|^[A-Za-z0-9+/]{40,}={0,2}$/;
|
|
28
|
+
// Credentials embedded in a URL authority: `user:pass@` or `:pass@` (empty user,
|
|
29
|
+
// as in `redis://:password@host`). The user part stops at the first colon, as in
|
|
30
|
+
// RFC 3986, so the split between user and password is fixed; letting it take colons
|
|
31
|
+
// made a long colon run with no `@` retry every split point, which is quadratic.
|
|
32
|
+
const URL_WITH_CREDENTIALS = /^[a-z][a-z0-9+.-]*:\/\/[^/@\s:]*:[^/@\s]+@/i;
|
|
33
|
+
// PEM private key blocks (certificates are public, so only PRIVATE KEY is masked).
|
|
34
|
+
const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/;
|
|
35
|
+
// URLs whose secret is carried in the host/path rather than a `user:pass@` authority:
|
|
36
|
+
// chat webhooks and error-tracking DSNs.
|
|
37
|
+
const SECRET_URL_PATTERN = /^https:\/\/hooks\.slack\.com\/services\/|^https:\/\/(?:discord|discordapp)\.com\/api\/webhooks\/|:\/\/[^/@\s]+@[^/\s]*\bsentry\.io\b/i;
|
|
38
|
+
// Well-known provider secret token formats, matched against the whole value. The
|
|
39
|
+
// prefixes are specific enough to have negligible false-positive risk, so they
|
|
40
|
+
// mask a value even under an innocuous key (e.g. `key: 'sk_live_...'`).
|
|
41
|
+
// `pk_` (publishable) keys are intentionally excluded — they are public by design.
|
|
42
|
+
const PROVIDER_SECRET_PATTERN = new RegExp([
|
|
43
|
+
'^(sk|rk)_(live|test)_[A-Za-z0-9]{8,}$',
|
|
44
|
+
'^whsec_[A-Za-z0-9]{16,}$',
|
|
45
|
+
'^(gh[posru]_|github_pat_)[A-Za-z0-9_]{20,}$',
|
|
46
|
+
'^xox[baprs]-[A-Za-z0-9-]{10,}$',
|
|
47
|
+
'^(AKIA|ASIA)[A-Z0-9]{16}$',
|
|
48
|
+
'^SG\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}$',
|
|
49
|
+
'^AIza[A-Za-z0-9_-]{20,}$',
|
|
50
|
+
'^ya29\\.[A-Za-z0-9_-]{20,}$',
|
|
51
|
+
'^glpat-[A-Za-z0-9_-]{16,}$',
|
|
52
|
+
'^(shpat|shpss|shpca|shppa)_[A-Za-z0-9]{20,}$',
|
|
53
|
+
'^dop_v1_[a-f0-9]{40,}$',
|
|
54
|
+
'^re_[A-Za-z0-9_]{16,}$',
|
|
55
|
+
'^nfp_[A-Za-z0-9]{20,}$',
|
|
56
|
+
'^EAAA[A-Za-z0-9_-]{20,}$'
|
|
57
|
+
].join('|'));
|
|
58
|
+
const looksSecret = (value)=>JWT_PATTERN.test(value) || LONG_TOKEN_PATTERN.test(value) || URL_WITH_CREDENTIALS.test(value) || PROVIDER_SECRET_PATTERN.test(value) || PEM_PRIVATE_KEY_PATTERN.test(value) || SECRET_URL_PATTERN.test(value);
|
|
59
|
+
const isPlainObject = (value)=>{
|
|
60
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const proto = Object.getPrototypeOf(value);
|
|
64
|
+
return proto === Object.prototype || proto === null;
|
|
65
|
+
};
|
|
66
|
+
// Accept either separator regardless of the host platform: `os.homedir()` and the app root come
|
|
67
|
+
// back with backslashes on Windows, and a dump may also carry POSIX-style paths (e.g. from
|
|
68
|
+
// config values), so a POSIX-only prefix check would ship absolute Windows paths verbatim.
|
|
69
|
+
const isPathPrefix = (value, prefix)=>{
|
|
70
|
+
if (value === prefix) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (!value.startsWith(prefix)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const next = value.charAt(prefix.length);
|
|
77
|
+
return next === '/' || next === '\\';
|
|
78
|
+
};
|
|
79
|
+
const relativize = (value, appRoot, homeDir)=>{
|
|
80
|
+
if (appRoot && isPathPrefix(value, appRoot)) {
|
|
81
|
+
return `<app>${value.slice(appRoot.length)}`;
|
|
82
|
+
}
|
|
83
|
+
if (homeDir && isPathPrefix(value, homeDir)) {
|
|
84
|
+
return `<home>${value.slice(homeDir.length)}`;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
};
|
|
88
|
+
// Characters that end a URL authority
|
|
89
|
+
const AUTHORITY_END = new Set([
|
|
90
|
+
'/',
|
|
91
|
+
'?',
|
|
92
|
+
'#',
|
|
93
|
+
' ',
|
|
94
|
+
'\t',
|
|
95
|
+
'\n',
|
|
96
|
+
'\r',
|
|
97
|
+
'"',
|
|
98
|
+
"'",
|
|
99
|
+
'`'
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* Masks the password of every `scheme://user:password@host` inside a longer string, such as a
|
|
103
|
+
* package.json script that inlines a database URL. Whole-value credential URLs are already
|
|
104
|
+
* caught by URL_WITH_CREDENTIALS; this covers the ones in the middle of a value. It is a single
|
|
105
|
+
* forward scan rather than a regex, because an unanchored pattern backtracks quadratically on
|
|
106
|
+
* long runs (the reason URL_WITH_CREDENTIALS is anchored and colon-free in the user part).
|
|
107
|
+
*/ const maskEmbeddedCredentials = (value)=>{
|
|
108
|
+
let result = '';
|
|
109
|
+
let copiedUpTo = 0;
|
|
110
|
+
let separator = value.indexOf('://');
|
|
111
|
+
while(separator !== -1){
|
|
112
|
+
const authorityStart = separator + 3;
|
|
113
|
+
let firstColon = -1;
|
|
114
|
+
let lastAt = -1;
|
|
115
|
+
let end = authorityStart;
|
|
116
|
+
while(end < value.length && !AUTHORITY_END.has(value[end])){
|
|
117
|
+
if (value[end] === ':' && firstColon === -1) {
|
|
118
|
+
firstColon = end;
|
|
119
|
+
} else if (value[end] === '@') {
|
|
120
|
+
lastAt = end;
|
|
121
|
+
}
|
|
122
|
+
end += 1;
|
|
123
|
+
}
|
|
124
|
+
// A colon before the last `@` separates user and password; one after it is a port.
|
|
125
|
+
if (firstColon !== -1 && lastAt > firstColon) {
|
|
126
|
+
result += `${value.slice(copiedUpTo, firstColon + 1)}${REDACTED}`;
|
|
127
|
+
copiedUpTo = lastAt;
|
|
128
|
+
}
|
|
129
|
+
separator = value.indexOf('://', end);
|
|
130
|
+
}
|
|
131
|
+
return copiedUpTo === 0 ? value : result + value.slice(copiedUpTo);
|
|
132
|
+
};
|
|
133
|
+
const deleteAtPath = (target, path)=>{
|
|
134
|
+
const segments = path.split('.');
|
|
135
|
+
let node = target;
|
|
136
|
+
for(let i = 0; i < segments.length - 1; i += 1){
|
|
137
|
+
if (!isPlainObject(node)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
node = node[segments[i]];
|
|
141
|
+
}
|
|
142
|
+
const lastKey = segments[segments.length - 1];
|
|
143
|
+
if (isPlainObject(node) && lastKey in node) {
|
|
144
|
+
node[lastKey] = REDACTED;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const walk = (value, appRoot, homeDir)=>{
|
|
148
|
+
if (typeof value === 'string') {
|
|
149
|
+
// Relativization is tried FIRST, and a value it rewrote is returned as-is. A string
|
|
150
|
+
// that starts with the app root or the home directory is a path, never a credential,
|
|
151
|
+
// whereas the reverse order let the secret heuristics claim one: a POSIX path made
|
|
152
|
+
// only of letters, digits and `/` is valid standard base64, so anything past 40
|
|
153
|
+
// characters matched LONG_TOKEN_PATTERN and shipped as `[REDACTED]` instead of
|
|
154
|
+
// `<app>/...`. Values that do not relativize still go through the full checks.
|
|
155
|
+
const relativized = relativize(value, appRoot, homeDir);
|
|
156
|
+
if (relativized !== value) {
|
|
157
|
+
return relativized;
|
|
158
|
+
}
|
|
159
|
+
return looksSecret(value) ? REDACTED : maskEmbeddedCredentials(value);
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
return value.map((item)=>walk(item, appRoot, homeDir));
|
|
163
|
+
}
|
|
164
|
+
if (isPlainObject(value)) {
|
|
165
|
+
const out = {};
|
|
166
|
+
for (const [key, val] of Object.entries(value)){
|
|
167
|
+
if (key === '__proto__') {
|
|
168
|
+
continue; // never write through the Object.prototype setter
|
|
169
|
+
}
|
|
170
|
+
// A secret-named key always collapses wholesale, regardless of the value's
|
|
171
|
+
// type. Recursing into it (e.g. for `apiToken: { salt, ... }`) would leak
|
|
172
|
+
// any inner value whose own key/value doesn't independently match a
|
|
173
|
+
// masking pattern (e.g. `providerOptions.auth.pass`), so there is nothing
|
|
174
|
+
// safe to preserve underneath a secret-named container.
|
|
175
|
+
out[key] = isSecretKey(key) ? REDACTED : walk(val, appRoot, homeDir);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
// Any other object type — Buffer, Date, Map, Set, class instances, etc. —
|
|
180
|
+
// is not a plain data container we can safely walk (Object.entries on a
|
|
181
|
+
// Buffer leaks its bytes, a Date serializes to `{}`), so redact defensively.
|
|
182
|
+
if (value !== null && typeof value === 'object') {
|
|
183
|
+
return REDACTED;
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
};
|
|
187
|
+
const scrub = (value, options = {})=>{
|
|
188
|
+
const cloned = walk(value, options.appRoot, options.homeDir);
|
|
189
|
+
for (const path of options.extraPaths ?? []){
|
|
190
|
+
deleteAtPath(cloned, path);
|
|
191
|
+
}
|
|
192
|
+
return cloned;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
exports.REDACTED = REDACTED;
|
|
196
|
+
exports.scrub = scrub;
|
|
197
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sources":["../../../../../../server/src/utils/debug-dump/redact.ts"],"sourcesContent":["export const REDACTED = '[REDACTED]';\n\n// Layer 1a: key names (substring match) whose value must be masked wherever they appear.\nconst SECRET_KEY_PATTERN =\n /(secret|password|passphrase|salt|token|api[_-]?key|access[_-]?key|license[_-]?key|private[_-]?key|encryption[_-]?key|credential|provider[_-]?options|connection[_-]?string|mnemonic|seed[_-]?phrase|authorization|bearer)/i;\n\n// Layer 1b: short, ambiguous key names matched EXACTLY (case-insensitive), so they\n// do not over-match as substrings (e.g. `pass` must not redact `compass`/`bypass`,\n// and this still catches a nested SMTP `auth.pass`).\nconst SECRET_KEY_EXACT = new Set(['pass', 'pwd', 'passwd', 'dsn']);\n// camelCase password suffixes (capitalized), e.g. `smtpAuthPass`, `dbPwd` — the\n// capital letter distinguishes them from lowercase look-alikes (`compass`, `bypass`).\nconst SECRET_KEY_SUFFIX = /[a-z](Pass|Pwd|Passwd)$/;\n// separator-delimited short secret names, e.g. `auth.pass`, `db_pwd`, `x-pass`.\nconst SECRET_KEY_DELIMITED = /(^|[._-])(pass|pwd|passwd|dsn)([._-]|$)/i;\n\nconst isSecretKey = (key: string): boolean =>\n SECRET_KEY_PATTERN.test(key) ||\n SECRET_KEY_EXACT.has(key.toLowerCase()) ||\n SECRET_KEY_SUFFIX.test(key) ||\n SECRET_KEY_DELIMITED.test(key);\n\n// Layer 3: string values that look like secrets regardless of their key.\n// Real JWTs are base64url-encoded and the header segment always begins with\n// `eyJ` (the base64url encoding of `{\"`), so requiring that prefix avoids\n// matching arbitrary three-part dotted strings like semver versions\n// (e.g. \"5.50.1\", \"18.3.1\").\nconst JWT_PATTERN = /^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\nconst LONG_TOKEN_PATTERN = /^[A-Fa-f0-9]{32,}$|^[A-Za-z0-9+/]{40,}={0,2}$/;\n// Credentials embedded in a URL authority: `user:pass@` or `:pass@` (empty user,\n// as in `redis://:password@host`). The user part stops at the first colon, as in\n// RFC 3986, so the split between user and password is fixed; letting it take colons\n// made a long colon run with no `@` retry every split point, which is quadratic.\nconst URL_WITH_CREDENTIALS = /^[a-z][a-z0-9+.-]*:\\/\\/[^/@\\s:]*:[^/@\\s]+@/i;\n// PEM private key blocks (certificates are public, so only PRIVATE KEY is masked).\nconst PEM_PRIVATE_KEY_PATTERN = /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/;\n// URLs whose secret is carried in the host/path rather than a `user:pass@` authority:\n// chat webhooks and error-tracking DSNs.\nconst SECRET_URL_PATTERN =\n /^https:\\/\\/hooks\\.slack\\.com\\/services\\/|^https:\\/\\/(?:discord|discordapp)\\.com\\/api\\/webhooks\\/|:\\/\\/[^/@\\s]+@[^/\\s]*\\bsentry\\.io\\b/i;\n\n// Well-known provider secret token formats, matched against the whole value. The\n// prefixes are specific enough to have negligible false-positive risk, so they\n// mask a value even under an innocuous key (e.g. `key: 'sk_live_...'`).\n// `pk_` (publishable) keys are intentionally excluded — they are public by design.\nconst PROVIDER_SECRET_PATTERN = new RegExp(\n [\n '^(sk|rk)_(live|test)_[A-Za-z0-9]{8,}$', // Stripe secret / restricted\n '^whsec_[A-Za-z0-9]{16,}$', // Stripe webhook signing secret\n '^(gh[posru]_|github_pat_)[A-Za-z0-9_]{20,}$', // GitHub tokens / fine-grained PAT\n '^xox[baprs]-[A-Za-z0-9-]{10,}$', // Slack tokens\n '^(AKIA|ASIA)[A-Z0-9]{16}$', // AWS access key id\n '^SG\\\\.[A-Za-z0-9_-]{16,}\\\\.[A-Za-z0-9_-]{16,}$', // SendGrid\n '^AIza[A-Za-z0-9_-]{20,}$', // Google API key\n '^ya29\\\\.[A-Za-z0-9_-]{20,}$', // Google OAuth access token\n '^glpat-[A-Za-z0-9_-]{16,}$', // GitLab personal access token\n '^(shpat|shpss|shpca|shppa)_[A-Za-z0-9]{20,}$', // Shopify\n '^dop_v1_[a-f0-9]{40,}$', // DigitalOcean\n '^re_[A-Za-z0-9_]{16,}$', // Resend\n '^nfp_[A-Za-z0-9]{20,}$', // Netlify\n '^EAAA[A-Za-z0-9_-]{20,}$', // Square\n ].join('|')\n);\n\nconst looksSecret = (value: string): boolean =>\n JWT_PATTERN.test(value) ||\n LONG_TOKEN_PATTERN.test(value) ||\n URL_WITH_CREDENTIALS.test(value) ||\n PROVIDER_SECRET_PATTERN.test(value) ||\n PEM_PRIVATE_KEY_PATTERN.test(value) ||\n SECRET_URL_PATTERN.test(value);\n\nexport interface ScrubOptions {\n appRoot?: string;\n homeDir?: string;\n extraPaths?: string[];\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n};\n\n// Accept either separator regardless of the host platform: `os.homedir()` and the app root come\n// back with backslashes on Windows, and a dump may also carry POSIX-style paths (e.g. from\n// config values), so a POSIX-only prefix check would ship absolute Windows paths verbatim.\nconst isPathPrefix = (value: string, prefix: string): boolean => {\n if (value === prefix) {\n return true;\n }\n if (!value.startsWith(prefix)) {\n return false;\n }\n const next = value.charAt(prefix.length);\n return next === '/' || next === '\\\\';\n};\n\nconst relativize = (value: string, appRoot?: string, homeDir?: string): string => {\n if (appRoot && isPathPrefix(value, appRoot)) {\n return `<app>${value.slice(appRoot.length)}`;\n }\n if (homeDir && isPathPrefix(value, homeDir)) {\n return `<home>${value.slice(homeDir.length)}`;\n }\n return value;\n};\n\n// Characters that end a URL authority\nconst AUTHORITY_END = new Set(['/', '?', '#', ' ', '\\t', '\\n', '\\r', '\"', \"'\", '`']);\n\n/**\n * Masks the password of every `scheme://user:password@host` inside a longer string, such as a\n * package.json script that inlines a database URL. Whole-value credential URLs are already\n * caught by URL_WITH_CREDENTIALS; this covers the ones in the middle of a value. It is a single\n * forward scan rather than a regex, because an unanchored pattern backtracks quadratically on\n * long runs (the reason URL_WITH_CREDENTIALS is anchored and colon-free in the user part).\n */\nconst maskEmbeddedCredentials = (value: string): string => {\n let result = '';\n let copiedUpTo = 0;\n let separator = value.indexOf('://');\n\n while (separator !== -1) {\n const authorityStart = separator + 3;\n let firstColon = -1;\n let lastAt = -1;\n let end = authorityStart;\n\n while (end < value.length && !AUTHORITY_END.has(value[end])) {\n if (value[end] === ':' && firstColon === -1) {\n firstColon = end;\n } else if (value[end] === '@') {\n lastAt = end;\n }\n end += 1;\n }\n\n // A colon before the last `@` separates user and password; one after it is a port.\n if (firstColon !== -1 && lastAt > firstColon) {\n result += `${value.slice(copiedUpTo, firstColon + 1)}${REDACTED}`;\n copiedUpTo = lastAt;\n }\n\n separator = value.indexOf('://', end);\n }\n\n return copiedUpTo === 0 ? value : result + value.slice(copiedUpTo);\n};\n\nconst deleteAtPath = (target: unknown, path: string): void => {\n const segments = path.split('.');\n let node: unknown = target;\n for (let i = 0; i < segments.length - 1; i += 1) {\n if (!isPlainObject(node)) {\n return;\n }\n node = node[segments[i]];\n }\n const lastKey = segments[segments.length - 1];\n if (isPlainObject(node) && lastKey in node) {\n node[lastKey] = REDACTED;\n }\n};\n\nconst walk = (value: unknown, appRoot?: string, homeDir?: string): unknown => {\n if (typeof value === 'string') {\n // Relativization is tried FIRST, and a value it rewrote is returned as-is. A string\n // that starts with the app root or the home directory is a path, never a credential,\n // whereas the reverse order let the secret heuristics claim one: a POSIX path made\n // only of letters, digits and `/` is valid standard base64, so anything past 40\n // characters matched LONG_TOKEN_PATTERN and shipped as `[REDACTED]` instead of\n // `<app>/...`. Values that do not relativize still go through the full checks.\n const relativized = relativize(value, appRoot, homeDir);\n if (relativized !== value) {\n return relativized;\n }\n return looksSecret(value) ? REDACTED : maskEmbeddedCredentials(value);\n }\n if (Array.isArray(value)) {\n return value.map((item) => walk(item, appRoot, homeDir));\n }\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n if (key === '__proto__') {\n continue; // never write through the Object.prototype setter\n }\n // A secret-named key always collapses wholesale, regardless of the value's\n // type. Recursing into it (e.g. for `apiToken: { salt, ... }`) would leak\n // any inner value whose own key/value doesn't independently match a\n // masking pattern (e.g. `providerOptions.auth.pass`), so there is nothing\n // safe to preserve underneath a secret-named container.\n out[key] = isSecretKey(key) ? REDACTED : walk(val, appRoot, homeDir);\n }\n return out;\n }\n // Any other object type — Buffer, Date, Map, Set, class instances, etc. —\n // is not a plain data container we can safely walk (Object.entries on a\n // Buffer leaks its bytes, a Date serializes to `{}`), so redact defensively.\n if (value !== null && typeof value === 'object') {\n return REDACTED;\n }\n return value;\n};\n\nexport const scrub = (value: unknown, options: ScrubOptions = {}): unknown => {\n const cloned = walk(value, options.appRoot, options.homeDir);\n for (const path of options.extraPaths ?? []) {\n deleteAtPath(cloned, path);\n }\n return cloned;\n};\n"],"names":["REDACTED","SECRET_KEY_PATTERN","SECRET_KEY_EXACT","Set","SECRET_KEY_SUFFIX","SECRET_KEY_DELIMITED","isSecretKey","key","test","has","toLowerCase","JWT_PATTERN","LONG_TOKEN_PATTERN","URL_WITH_CREDENTIALS","PEM_PRIVATE_KEY_PATTERN","SECRET_URL_PATTERN","PROVIDER_SECRET_PATTERN","RegExp","join","looksSecret","value","isPlainObject","Array","isArray","proto","Object","getPrototypeOf","prototype","isPathPrefix","prefix","startsWith","next","charAt","length","relativize","appRoot","homeDir","slice","AUTHORITY_END","maskEmbeddedCredentials","result","copiedUpTo","separator","indexOf","authorityStart","firstColon","lastAt","end","deleteAtPath","target","path","segments","split","node","i","lastKey","walk","relativized","map","item","out","val","entries","scrub","options","cloned","extraPaths"],"mappings":"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"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const REDACTED = '[REDACTED]';
|
|
2
|
+
// Layer 1a: key names (substring match) whose value must be masked wherever they appear.
|
|
3
|
+
const SECRET_KEY_PATTERN = /(secret|password|passphrase|salt|token|api[_-]?key|access[_-]?key|license[_-]?key|private[_-]?key|encryption[_-]?key|credential|provider[_-]?options|connection[_-]?string|mnemonic|seed[_-]?phrase|authorization|bearer)/i;
|
|
4
|
+
// Layer 1b: short, ambiguous key names matched EXACTLY (case-insensitive), so they
|
|
5
|
+
// do not over-match as substrings (e.g. `pass` must not redact `compass`/`bypass`,
|
|
6
|
+
// and this still catches a nested SMTP `auth.pass`).
|
|
7
|
+
const SECRET_KEY_EXACT = new Set([
|
|
8
|
+
'pass',
|
|
9
|
+
'pwd',
|
|
10
|
+
'passwd',
|
|
11
|
+
'dsn'
|
|
12
|
+
]);
|
|
13
|
+
// camelCase password suffixes (capitalized), e.g. `smtpAuthPass`, `dbPwd` — the
|
|
14
|
+
// capital letter distinguishes them from lowercase look-alikes (`compass`, `bypass`).
|
|
15
|
+
const SECRET_KEY_SUFFIX = /[a-z](Pass|Pwd|Passwd)$/;
|
|
16
|
+
// separator-delimited short secret names, e.g. `auth.pass`, `db_pwd`, `x-pass`.
|
|
17
|
+
const SECRET_KEY_DELIMITED = /(^|[._-])(pass|pwd|passwd|dsn)([._-]|$)/i;
|
|
18
|
+
const isSecretKey = (key)=>SECRET_KEY_PATTERN.test(key) || SECRET_KEY_EXACT.has(key.toLowerCase()) || SECRET_KEY_SUFFIX.test(key) || SECRET_KEY_DELIMITED.test(key);
|
|
19
|
+
// Layer 3: string values that look like secrets regardless of their key.
|
|
20
|
+
// Real JWTs are base64url-encoded and the header segment always begins with
|
|
21
|
+
// `eyJ` (the base64url encoding of `{"`), so requiring that prefix avoids
|
|
22
|
+
// matching arbitrary three-part dotted strings like semver versions
|
|
23
|
+
// (e.g. "5.50.1", "18.3.1").
|
|
24
|
+
const JWT_PATTERN = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
25
|
+
const LONG_TOKEN_PATTERN = /^[A-Fa-f0-9]{32,}$|^[A-Za-z0-9+/]{40,}={0,2}$/;
|
|
26
|
+
// Credentials embedded in a URL authority: `user:pass@` or `:pass@` (empty user,
|
|
27
|
+
// as in `redis://:password@host`). The user part stops at the first colon, as in
|
|
28
|
+
// RFC 3986, so the split between user and password is fixed; letting it take colons
|
|
29
|
+
// made a long colon run with no `@` retry every split point, which is quadratic.
|
|
30
|
+
const URL_WITH_CREDENTIALS = /^[a-z][a-z0-9+.-]*:\/\/[^/@\s:]*:[^/@\s]+@/i;
|
|
31
|
+
// PEM private key blocks (certificates are public, so only PRIVATE KEY is masked).
|
|
32
|
+
const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/;
|
|
33
|
+
// URLs whose secret is carried in the host/path rather than a `user:pass@` authority:
|
|
34
|
+
// chat webhooks and error-tracking DSNs.
|
|
35
|
+
const SECRET_URL_PATTERN = /^https:\/\/hooks\.slack\.com\/services\/|^https:\/\/(?:discord|discordapp)\.com\/api\/webhooks\/|:\/\/[^/@\s]+@[^/\s]*\bsentry\.io\b/i;
|
|
36
|
+
// Well-known provider secret token formats, matched against the whole value. The
|
|
37
|
+
// prefixes are specific enough to have negligible false-positive risk, so they
|
|
38
|
+
// mask a value even under an innocuous key (e.g. `key: 'sk_live_...'`).
|
|
39
|
+
// `pk_` (publishable) keys are intentionally excluded — they are public by design.
|
|
40
|
+
const PROVIDER_SECRET_PATTERN = new RegExp([
|
|
41
|
+
'^(sk|rk)_(live|test)_[A-Za-z0-9]{8,}$',
|
|
42
|
+
'^whsec_[A-Za-z0-9]{16,}$',
|
|
43
|
+
'^(gh[posru]_|github_pat_)[A-Za-z0-9_]{20,}$',
|
|
44
|
+
'^xox[baprs]-[A-Za-z0-9-]{10,}$',
|
|
45
|
+
'^(AKIA|ASIA)[A-Z0-9]{16}$',
|
|
46
|
+
'^SG\\.[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{16,}$',
|
|
47
|
+
'^AIza[A-Za-z0-9_-]{20,}$',
|
|
48
|
+
'^ya29\\.[A-Za-z0-9_-]{20,}$',
|
|
49
|
+
'^glpat-[A-Za-z0-9_-]{16,}$',
|
|
50
|
+
'^(shpat|shpss|shpca|shppa)_[A-Za-z0-9]{20,}$',
|
|
51
|
+
'^dop_v1_[a-f0-9]{40,}$',
|
|
52
|
+
'^re_[A-Za-z0-9_]{16,}$',
|
|
53
|
+
'^nfp_[A-Za-z0-9]{20,}$',
|
|
54
|
+
'^EAAA[A-Za-z0-9_-]{20,}$'
|
|
55
|
+
].join('|'));
|
|
56
|
+
const looksSecret = (value)=>JWT_PATTERN.test(value) || LONG_TOKEN_PATTERN.test(value) || URL_WITH_CREDENTIALS.test(value) || PROVIDER_SECRET_PATTERN.test(value) || PEM_PRIVATE_KEY_PATTERN.test(value) || SECRET_URL_PATTERN.test(value);
|
|
57
|
+
const isPlainObject = (value)=>{
|
|
58
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const proto = Object.getPrototypeOf(value);
|
|
62
|
+
return proto === Object.prototype || proto === null;
|
|
63
|
+
};
|
|
64
|
+
// Accept either separator regardless of the host platform: `os.homedir()` and the app root come
|
|
65
|
+
// back with backslashes on Windows, and a dump may also carry POSIX-style paths (e.g. from
|
|
66
|
+
// config values), so a POSIX-only prefix check would ship absolute Windows paths verbatim.
|
|
67
|
+
const isPathPrefix = (value, prefix)=>{
|
|
68
|
+
if (value === prefix) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (!value.startsWith(prefix)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const next = value.charAt(prefix.length);
|
|
75
|
+
return next === '/' || next === '\\';
|
|
76
|
+
};
|
|
77
|
+
const relativize = (value, appRoot, homeDir)=>{
|
|
78
|
+
if (appRoot && isPathPrefix(value, appRoot)) {
|
|
79
|
+
return `<app>${value.slice(appRoot.length)}`;
|
|
80
|
+
}
|
|
81
|
+
if (homeDir && isPathPrefix(value, homeDir)) {
|
|
82
|
+
return `<home>${value.slice(homeDir.length)}`;
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
};
|
|
86
|
+
// Characters that end a URL authority
|
|
87
|
+
const AUTHORITY_END = new Set([
|
|
88
|
+
'/',
|
|
89
|
+
'?',
|
|
90
|
+
'#',
|
|
91
|
+
' ',
|
|
92
|
+
'\t',
|
|
93
|
+
'\n',
|
|
94
|
+
'\r',
|
|
95
|
+
'"',
|
|
96
|
+
"'",
|
|
97
|
+
'`'
|
|
98
|
+
]);
|
|
99
|
+
/**
|
|
100
|
+
* Masks the password of every `scheme://user:password@host` inside a longer string, such as a
|
|
101
|
+
* package.json script that inlines a database URL. Whole-value credential URLs are already
|
|
102
|
+
* caught by URL_WITH_CREDENTIALS; this covers the ones in the middle of a value. It is a single
|
|
103
|
+
* forward scan rather than a regex, because an unanchored pattern backtracks quadratically on
|
|
104
|
+
* long runs (the reason URL_WITH_CREDENTIALS is anchored and colon-free in the user part).
|
|
105
|
+
*/ const maskEmbeddedCredentials = (value)=>{
|
|
106
|
+
let result = '';
|
|
107
|
+
let copiedUpTo = 0;
|
|
108
|
+
let separator = value.indexOf('://');
|
|
109
|
+
while(separator !== -1){
|
|
110
|
+
const authorityStart = separator + 3;
|
|
111
|
+
let firstColon = -1;
|
|
112
|
+
let lastAt = -1;
|
|
113
|
+
let end = authorityStart;
|
|
114
|
+
while(end < value.length && !AUTHORITY_END.has(value[end])){
|
|
115
|
+
if (value[end] === ':' && firstColon === -1) {
|
|
116
|
+
firstColon = end;
|
|
117
|
+
} else if (value[end] === '@') {
|
|
118
|
+
lastAt = end;
|
|
119
|
+
}
|
|
120
|
+
end += 1;
|
|
121
|
+
}
|
|
122
|
+
// A colon before the last `@` separates user and password; one after it is a port.
|
|
123
|
+
if (firstColon !== -1 && lastAt > firstColon) {
|
|
124
|
+
result += `${value.slice(copiedUpTo, firstColon + 1)}${REDACTED}`;
|
|
125
|
+
copiedUpTo = lastAt;
|
|
126
|
+
}
|
|
127
|
+
separator = value.indexOf('://', end);
|
|
128
|
+
}
|
|
129
|
+
return copiedUpTo === 0 ? value : result + value.slice(copiedUpTo);
|
|
130
|
+
};
|
|
131
|
+
const deleteAtPath = (target, path)=>{
|
|
132
|
+
const segments = path.split('.');
|
|
133
|
+
let node = target;
|
|
134
|
+
for(let i = 0; i < segments.length - 1; i += 1){
|
|
135
|
+
if (!isPlainObject(node)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
node = node[segments[i]];
|
|
139
|
+
}
|
|
140
|
+
const lastKey = segments[segments.length - 1];
|
|
141
|
+
if (isPlainObject(node) && lastKey in node) {
|
|
142
|
+
node[lastKey] = REDACTED;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const walk = (value, appRoot, homeDir)=>{
|
|
146
|
+
if (typeof value === 'string') {
|
|
147
|
+
// Relativization is tried FIRST, and a value it rewrote is returned as-is. A string
|
|
148
|
+
// that starts with the app root or the home directory is a path, never a credential,
|
|
149
|
+
// whereas the reverse order let the secret heuristics claim one: a POSIX path made
|
|
150
|
+
// only of letters, digits and `/` is valid standard base64, so anything past 40
|
|
151
|
+
// characters matched LONG_TOKEN_PATTERN and shipped as `[REDACTED]` instead of
|
|
152
|
+
// `<app>/...`. Values that do not relativize still go through the full checks.
|
|
153
|
+
const relativized = relativize(value, appRoot, homeDir);
|
|
154
|
+
if (relativized !== value) {
|
|
155
|
+
return relativized;
|
|
156
|
+
}
|
|
157
|
+
return looksSecret(value) ? REDACTED : maskEmbeddedCredentials(value);
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
return value.map((item)=>walk(item, appRoot, homeDir));
|
|
161
|
+
}
|
|
162
|
+
if (isPlainObject(value)) {
|
|
163
|
+
const out = {};
|
|
164
|
+
for (const [key, val] of Object.entries(value)){
|
|
165
|
+
if (key === '__proto__') {
|
|
166
|
+
continue; // never write through the Object.prototype setter
|
|
167
|
+
}
|
|
168
|
+
// A secret-named key always collapses wholesale, regardless of the value's
|
|
169
|
+
// type. Recursing into it (e.g. for `apiToken: { salt, ... }`) would leak
|
|
170
|
+
// any inner value whose own key/value doesn't independently match a
|
|
171
|
+
// masking pattern (e.g. `providerOptions.auth.pass`), so there is nothing
|
|
172
|
+
// safe to preserve underneath a secret-named container.
|
|
173
|
+
out[key] = isSecretKey(key) ? REDACTED : walk(val, appRoot, homeDir);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
// Any other object type — Buffer, Date, Map, Set, class instances, etc. —
|
|
178
|
+
// is not a plain data container we can safely walk (Object.entries on a
|
|
179
|
+
// Buffer leaks its bytes, a Date serializes to `{}`), so redact defensively.
|
|
180
|
+
if (value !== null && typeof value === 'object') {
|
|
181
|
+
return REDACTED;
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
};
|
|
185
|
+
const scrub = (value, options = {})=>{
|
|
186
|
+
const cloned = walk(value, options.appRoot, options.homeDir);
|
|
187
|
+
for (const path of options.extraPaths ?? []){
|
|
188
|
+
deleteAtPath(cloned, path);
|
|
189
|
+
}
|
|
190
|
+
return cloned;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export { REDACTED, scrub };
|
|
194
|
+
//# sourceMappingURL=redact.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.mjs","sources":["../../../../../../server/src/utils/debug-dump/redact.ts"],"sourcesContent":["export const REDACTED = '[REDACTED]';\n\n// Layer 1a: key names (substring match) whose value must be masked wherever they appear.\nconst SECRET_KEY_PATTERN =\n /(secret|password|passphrase|salt|token|api[_-]?key|access[_-]?key|license[_-]?key|private[_-]?key|encryption[_-]?key|credential|provider[_-]?options|connection[_-]?string|mnemonic|seed[_-]?phrase|authorization|bearer)/i;\n\n// Layer 1b: short, ambiguous key names matched EXACTLY (case-insensitive), so they\n// do not over-match as substrings (e.g. `pass` must not redact `compass`/`bypass`,\n// and this still catches a nested SMTP `auth.pass`).\nconst SECRET_KEY_EXACT = new Set(['pass', 'pwd', 'passwd', 'dsn']);\n// camelCase password suffixes (capitalized), e.g. `smtpAuthPass`, `dbPwd` — the\n// capital letter distinguishes them from lowercase look-alikes (`compass`, `bypass`).\nconst SECRET_KEY_SUFFIX = /[a-z](Pass|Pwd|Passwd)$/;\n// separator-delimited short secret names, e.g. `auth.pass`, `db_pwd`, `x-pass`.\nconst SECRET_KEY_DELIMITED = /(^|[._-])(pass|pwd|passwd|dsn)([._-]|$)/i;\n\nconst isSecretKey = (key: string): boolean =>\n SECRET_KEY_PATTERN.test(key) ||\n SECRET_KEY_EXACT.has(key.toLowerCase()) ||\n SECRET_KEY_SUFFIX.test(key) ||\n SECRET_KEY_DELIMITED.test(key);\n\n// Layer 3: string values that look like secrets regardless of their key.\n// Real JWTs are base64url-encoded and the header segment always begins with\n// `eyJ` (the base64url encoding of `{\"`), so requiring that prefix avoids\n// matching arbitrary three-part dotted strings like semver versions\n// (e.g. \"5.50.1\", \"18.3.1\").\nconst JWT_PATTERN = /^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\nconst LONG_TOKEN_PATTERN = /^[A-Fa-f0-9]{32,}$|^[A-Za-z0-9+/]{40,}={0,2}$/;\n// Credentials embedded in a URL authority: `user:pass@` or `:pass@` (empty user,\n// as in `redis://:password@host`). The user part stops at the first colon, as in\n// RFC 3986, so the split between user and password is fixed; letting it take colons\n// made a long colon run with no `@` retry every split point, which is quadratic.\nconst URL_WITH_CREDENTIALS = /^[a-z][a-z0-9+.-]*:\\/\\/[^/@\\s:]*:[^/@\\s]+@/i;\n// PEM private key blocks (certificates are public, so only PRIVATE KEY is masked).\nconst PEM_PRIVATE_KEY_PATTERN = /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/;\n// URLs whose secret is carried in the host/path rather than a `user:pass@` authority:\n// chat webhooks and error-tracking DSNs.\nconst SECRET_URL_PATTERN =\n /^https:\\/\\/hooks\\.slack\\.com\\/services\\/|^https:\\/\\/(?:discord|discordapp)\\.com\\/api\\/webhooks\\/|:\\/\\/[^/@\\s]+@[^/\\s]*\\bsentry\\.io\\b/i;\n\n// Well-known provider secret token formats, matched against the whole value. The\n// prefixes are specific enough to have negligible false-positive risk, so they\n// mask a value even under an innocuous key (e.g. `key: 'sk_live_...'`).\n// `pk_` (publishable) keys are intentionally excluded — they are public by design.\nconst PROVIDER_SECRET_PATTERN = new RegExp(\n [\n '^(sk|rk)_(live|test)_[A-Za-z0-9]{8,}$', // Stripe secret / restricted\n '^whsec_[A-Za-z0-9]{16,}$', // Stripe webhook signing secret\n '^(gh[posru]_|github_pat_)[A-Za-z0-9_]{20,}$', // GitHub tokens / fine-grained PAT\n '^xox[baprs]-[A-Za-z0-9-]{10,}$', // Slack tokens\n '^(AKIA|ASIA)[A-Z0-9]{16}$', // AWS access key id\n '^SG\\\\.[A-Za-z0-9_-]{16,}\\\\.[A-Za-z0-9_-]{16,}$', // SendGrid\n '^AIza[A-Za-z0-9_-]{20,}$', // Google API key\n '^ya29\\\\.[A-Za-z0-9_-]{20,}$', // Google OAuth access token\n '^glpat-[A-Za-z0-9_-]{16,}$', // GitLab personal access token\n '^(shpat|shpss|shpca|shppa)_[A-Za-z0-9]{20,}$', // Shopify\n '^dop_v1_[a-f0-9]{40,}$', // DigitalOcean\n '^re_[A-Za-z0-9_]{16,}$', // Resend\n '^nfp_[A-Za-z0-9]{20,}$', // Netlify\n '^EAAA[A-Za-z0-9_-]{20,}$', // Square\n ].join('|')\n);\n\nconst looksSecret = (value: string): boolean =>\n JWT_PATTERN.test(value) ||\n LONG_TOKEN_PATTERN.test(value) ||\n URL_WITH_CREDENTIALS.test(value) ||\n PROVIDER_SECRET_PATTERN.test(value) ||\n PEM_PRIVATE_KEY_PATTERN.test(value) ||\n SECRET_URL_PATTERN.test(value);\n\nexport interface ScrubOptions {\n appRoot?: string;\n homeDir?: string;\n extraPaths?: string[];\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n};\n\n// Accept either separator regardless of the host platform: `os.homedir()` and the app root come\n// back with backslashes on Windows, and a dump may also carry POSIX-style paths (e.g. from\n// config values), so a POSIX-only prefix check would ship absolute Windows paths verbatim.\nconst isPathPrefix = (value: string, prefix: string): boolean => {\n if (value === prefix) {\n return true;\n }\n if (!value.startsWith(prefix)) {\n return false;\n }\n const next = value.charAt(prefix.length);\n return next === '/' || next === '\\\\';\n};\n\nconst relativize = (value: string, appRoot?: string, homeDir?: string): string => {\n if (appRoot && isPathPrefix(value, appRoot)) {\n return `<app>${value.slice(appRoot.length)}`;\n }\n if (homeDir && isPathPrefix(value, homeDir)) {\n return `<home>${value.slice(homeDir.length)}`;\n }\n return value;\n};\n\n// Characters that end a URL authority\nconst AUTHORITY_END = new Set(['/', '?', '#', ' ', '\\t', '\\n', '\\r', '\"', \"'\", '`']);\n\n/**\n * Masks the password of every `scheme://user:password@host` inside a longer string, such as a\n * package.json script that inlines a database URL. Whole-value credential URLs are already\n * caught by URL_WITH_CREDENTIALS; this covers the ones in the middle of a value. It is a single\n * forward scan rather than a regex, because an unanchored pattern backtracks quadratically on\n * long runs (the reason URL_WITH_CREDENTIALS is anchored and colon-free in the user part).\n */\nconst maskEmbeddedCredentials = (value: string): string => {\n let result = '';\n let copiedUpTo = 0;\n let separator = value.indexOf('://');\n\n while (separator !== -1) {\n const authorityStart = separator + 3;\n let firstColon = -1;\n let lastAt = -1;\n let end = authorityStart;\n\n while (end < value.length && !AUTHORITY_END.has(value[end])) {\n if (value[end] === ':' && firstColon === -1) {\n firstColon = end;\n } else if (value[end] === '@') {\n lastAt = end;\n }\n end += 1;\n }\n\n // A colon before the last `@` separates user and password; one after it is a port.\n if (firstColon !== -1 && lastAt > firstColon) {\n result += `${value.slice(copiedUpTo, firstColon + 1)}${REDACTED}`;\n copiedUpTo = lastAt;\n }\n\n separator = value.indexOf('://', end);\n }\n\n return copiedUpTo === 0 ? value : result + value.slice(copiedUpTo);\n};\n\nconst deleteAtPath = (target: unknown, path: string): void => {\n const segments = path.split('.');\n let node: unknown = target;\n for (let i = 0; i < segments.length - 1; i += 1) {\n if (!isPlainObject(node)) {\n return;\n }\n node = node[segments[i]];\n }\n const lastKey = segments[segments.length - 1];\n if (isPlainObject(node) && lastKey in node) {\n node[lastKey] = REDACTED;\n }\n};\n\nconst walk = (value: unknown, appRoot?: string, homeDir?: string): unknown => {\n if (typeof value === 'string') {\n // Relativization is tried FIRST, and a value it rewrote is returned as-is. A string\n // that starts with the app root or the home directory is a path, never a credential,\n // whereas the reverse order let the secret heuristics claim one: a POSIX path made\n // only of letters, digits and `/` is valid standard base64, so anything past 40\n // characters matched LONG_TOKEN_PATTERN and shipped as `[REDACTED]` instead of\n // `<app>/...`. Values that do not relativize still go through the full checks.\n const relativized = relativize(value, appRoot, homeDir);\n if (relativized !== value) {\n return relativized;\n }\n return looksSecret(value) ? REDACTED : maskEmbeddedCredentials(value);\n }\n if (Array.isArray(value)) {\n return value.map((item) => walk(item, appRoot, homeDir));\n }\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n if (key === '__proto__') {\n continue; // never write through the Object.prototype setter\n }\n // A secret-named key always collapses wholesale, regardless of the value's\n // type. Recursing into it (e.g. for `apiToken: { salt, ... }`) would leak\n // any inner value whose own key/value doesn't independently match a\n // masking pattern (e.g. `providerOptions.auth.pass`), so there is nothing\n // safe to preserve underneath a secret-named container.\n out[key] = isSecretKey(key) ? REDACTED : walk(val, appRoot, homeDir);\n }\n return out;\n }\n // Any other object type — Buffer, Date, Map, Set, class instances, etc. —\n // is not a plain data container we can safely walk (Object.entries on a\n // Buffer leaks its bytes, a Date serializes to `{}`), so redact defensively.\n if (value !== null && typeof value === 'object') {\n return REDACTED;\n }\n return value;\n};\n\nexport const scrub = (value: unknown, options: ScrubOptions = {}): unknown => {\n const cloned = walk(value, options.appRoot, options.homeDir);\n for (const path of options.extraPaths ?? []) {\n deleteAtPath(cloned, path);\n }\n return cloned;\n};\n"],"names":["REDACTED","SECRET_KEY_PATTERN","SECRET_KEY_EXACT","Set","SECRET_KEY_SUFFIX","SECRET_KEY_DELIMITED","isSecretKey","key","test","has","toLowerCase","JWT_PATTERN","LONG_TOKEN_PATTERN","URL_WITH_CREDENTIALS","PEM_PRIVATE_KEY_PATTERN","SECRET_URL_PATTERN","PROVIDER_SECRET_PATTERN","RegExp","join","looksSecret","value","isPlainObject","Array","isArray","proto","Object","getPrototypeOf","prototype","isPathPrefix","prefix","startsWith","next","charAt","length","relativize","appRoot","homeDir","slice","AUTHORITY_END","maskEmbeddedCredentials","result","copiedUpTo","separator","indexOf","authorityStart","firstColon","lastAt","end","deleteAtPath","target","path","segments","split","node","i","lastKey","walk","relativized","map","item","out","val","entries","scrub","options","cloned","extraPaths"],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the plan label from the license information returned by the
|
|
5
|
+
* `/admin/project-type` endpoint (browser) or `strapi.ee` (server).
|
|
6
|
+
*
|
|
7
|
+
* This labels the edition that is running: it follows `isEE`, so a lapsed license reads
|
|
8
|
+
* Community. Browser tracking and NPS report this label and the startup table applies the same
|
|
9
|
+
* rule; server metrics also label the running edition, but never say Growth. For the plan a
|
|
10
|
+
* license was issued for, lapsed or not (the Plan card, the Support card and the debug dump),
|
|
11
|
+
* use `getLicensedPlan` below.
|
|
12
|
+
*
|
|
13
|
+
* The label is intentionally limited to three values:
|
|
14
|
+
* - `Community` when there is no license (Community Edition).
|
|
15
|
+
* - `Growth` when the licensed plan price id contains "growth".
|
|
16
|
+
* - `Enterprise` for any other licensed plan.
|
|
17
|
+
*/ const getProjectType = ({ isEE, planPriceId })=>{
|
|
18
|
+
if (!isEE) {
|
|
19
|
+
return 'Community';
|
|
20
|
+
}
|
|
21
|
+
if (planPriceId?.toLowerCase().includes('growth')) {
|
|
22
|
+
return 'Growth';
|
|
23
|
+
}
|
|
24
|
+
return 'Enterprise';
|
|
25
|
+
};
|
|
26
|
+
// The statuses a license can be in once it has been read. `none` (never licensed) is left out
|
|
27
|
+
// on purpose, and so is a missing status, which a mixed-version install can produce.
|
|
28
|
+
const LICENSED_STATUSES = [
|
|
29
|
+
'active',
|
|
30
|
+
'expired',
|
|
31
|
+
'unknown'
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the plan a license was issued for, including one that has lapsed or can no longer
|
|
35
|
+
* be read, so Support links, the Plan card and the debug dump can still name it. It unlocks
|
|
36
|
+
* nothing: feature gating stays on `isEE`.
|
|
37
|
+
*
|
|
38
|
+
* A plan is only reported when a type was verified at some point (live, or retained once the
|
|
39
|
+
* license lapsed) AND the status is licensed. A corrupt license file sets `unknown` before any
|
|
40
|
+
* type is stored, and with no price id `getProjectType` would fall through to Enterprise, so
|
|
41
|
+
* both conditions are required. Every caller that derives a plan should go through here.
|
|
42
|
+
*/ const getLicensedPlan = ({ licenseStatus, type, planPriceId })=>getProjectType({
|
|
43
|
+
isEE: Boolean(type) && LICENSED_STATUSES.includes(licenseStatus ?? ''),
|
|
44
|
+
planPriceId: planPriceId ?? undefined
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
exports.getLicensedPlan = getLicensedPlan;
|
|
48
|
+
exports.getProjectType = getProjectType;
|
|
49
|
+
//# sourceMappingURL=get-project-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-project-type.js","sources":["../../../../shared/utils/get-project-type.ts"],"sourcesContent":["type ProjectType = 'Community' | 'Growth' | 'Enterprise';\n\n/**\n * Resolves the plan label from the license information returned by the\n * `/admin/project-type` endpoint (browser) or `strapi.ee` (server).\n *\n * This labels the edition that is running: it follows `isEE`, so a lapsed license reads\n * Community. Browser tracking and NPS report this label and the startup table applies the same\n * rule; server metrics also label the running edition, but never say Growth. For the plan a\n * license was issued for, lapsed or not (the Plan card, the Support card and the debug dump),\n * use `getLicensedPlan` below.\n *\n * The label is intentionally limited to three values:\n * - `Community` when there is no license (Community Edition).\n * - `Growth` when the licensed plan price id contains \"growth\".\n * - `Enterprise` for any other licensed plan.\n */\nconst getProjectType = ({\n isEE,\n planPriceId,\n}: {\n isEE: boolean;\n planPriceId?: string;\n}): ProjectType => {\n if (!isEE) {\n return 'Community';\n }\n\n if (planPriceId?.toLowerCase().includes('growth')) {\n return 'Growth';\n }\n\n return 'Enterprise';\n};\n\n// The statuses a license can be in once it has been read. `none` (never licensed) is left out\n// on purpose, and so is a missing status, which a mixed-version install can produce.\nconst LICENSED_STATUSES: readonly string[] = ['active', 'expired', 'unknown'];\n\n/**\n * Resolves the plan a license was issued for, including one that has lapsed or can no longer\n * be read, so Support links, the Plan card and the debug dump can still name it. It unlocks\n * nothing: feature gating stays on `isEE`.\n *\n * A plan is only reported when a type was verified at some point (live, or retained once the\n * license lapsed) AND the status is licensed. A corrupt license file sets `unknown` before any\n * type is stored, and with no price id `getProjectType` would fall through to Enterprise, so\n * both conditions are required. Every caller that derives a plan should go through here.\n */\nconst getLicensedPlan = ({\n licenseStatus,\n type,\n planPriceId,\n}: {\n licenseStatus: string | null | undefined;\n type: string | null | undefined;\n planPriceId?: string | null;\n}): ProjectType =>\n getProjectType({\n isEE: Boolean(type) && LICENSED_STATUSES.includes(licenseStatus ?? ''),\n planPriceId: planPriceId ?? undefined,\n });\n\nexport { getLicensedPlan, getProjectType };\nexport type { ProjectType };\n"],"names":["getProjectType","isEE","planPriceId","toLowerCase","includes","LICENSED_STATUSES","getLicensedPlan","licenseStatus","type","Boolean","undefined"],"mappings":";;AAEA;;;;;;;;;;;;;;AAcC,UACKA,cAAAA,GAAiB,CAAC,EACtBC,IAAI,EACJC,WAAW,EAIZ,GAAA;AACC,IAAA,IAAI,CAACD,IAAAA,EAAM;QACT,OAAO,WAAA;AACT,IAAA;IAEA,IAAIC,WAAAA,EAAaC,WAAAA,EAAAA,CAAcC,QAAAA,CAAS,QAAA,CAAA,EAAW;QACjD,OAAO,QAAA;AACT,IAAA;IAEA,OAAO,YAAA;AACT;AAEA;AACA;AACA,MAAMC,iBAAAA,GAAuC;AAAC,IAAA,QAAA;AAAU,IAAA,SAAA;AAAW,IAAA;AAAU,CAAA;AAE7E;;;;;;;;;IAUA,MAAMC,eAAAA,GAAkB,CAAC,EACvBC,aAAa,EACbC,IAAI,EACJN,WAAW,EAKZ,GACCF,cAAAA,CAAe;AACbC,QAAAA,IAAAA,EAAMQ,OAAAA,CAAQD,IAAAA,CAAAA,IAASH,iBAAAA,CAAkBD,QAAQ,CAACG,aAAAA,IAAiB,EAAA,CAAA;AACnEL,QAAAA,WAAAA,EAAaA,WAAAA,IAAeQ;AAC9B,KAAA;;;;;"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the plan label from the license information returned by the
|
|
3
|
+
* `/admin/project-type` endpoint (browser) or `strapi.ee` (server).
|
|
4
|
+
*
|
|
5
|
+
* This labels the edition that is running: it follows `isEE`, so a lapsed license reads
|
|
6
|
+
* Community. Browser tracking and NPS report this label and the startup table applies the same
|
|
7
|
+
* rule; server metrics also label the running edition, but never say Growth. For the plan a
|
|
8
|
+
* license was issued for, lapsed or not (the Plan card, the Support card and the debug dump),
|
|
9
|
+
* use `getLicensedPlan` below.
|
|
10
|
+
*
|
|
11
|
+
* The label is intentionally limited to three values:
|
|
12
|
+
* - `Community` when there is no license (Community Edition).
|
|
13
|
+
* - `Growth` when the licensed plan price id contains "growth".
|
|
14
|
+
* - `Enterprise` for any other licensed plan.
|
|
15
|
+
*/ const getProjectType = ({ isEE, planPriceId })=>{
|
|
16
|
+
if (!isEE) {
|
|
17
|
+
return 'Community';
|
|
18
|
+
}
|
|
19
|
+
if (planPriceId?.toLowerCase().includes('growth')) {
|
|
20
|
+
return 'Growth';
|
|
21
|
+
}
|
|
22
|
+
return 'Enterprise';
|
|
23
|
+
};
|
|
24
|
+
// The statuses a license can be in once it has been read. `none` (never licensed) is left out
|
|
25
|
+
// on purpose, and so is a missing status, which a mixed-version install can produce.
|
|
26
|
+
const LICENSED_STATUSES = [
|
|
27
|
+
'active',
|
|
28
|
+
'expired',
|
|
29
|
+
'unknown'
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the plan a license was issued for, including one that has lapsed or can no longer
|
|
33
|
+
* be read, so Support links, the Plan card and the debug dump can still name it. It unlocks
|
|
34
|
+
* nothing: feature gating stays on `isEE`.
|
|
35
|
+
*
|
|
36
|
+
* A plan is only reported when a type was verified at some point (live, or retained once the
|
|
37
|
+
* license lapsed) AND the status is licensed. A corrupt license file sets `unknown` before any
|
|
38
|
+
* type is stored, and with no price id `getProjectType` would fall through to Enterprise, so
|
|
39
|
+
* both conditions are required. Every caller that derives a plan should go through here.
|
|
40
|
+
*/ const getLicensedPlan = ({ licenseStatus, type, planPriceId })=>getProjectType({
|
|
41
|
+
isEE: Boolean(type) && LICENSED_STATUSES.includes(licenseStatus ?? ''),
|
|
42
|
+
planPriceId: planPriceId ?? undefined
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export { getLicensedPlan, getProjectType };
|
|
46
|
+
//# sourceMappingURL=get-project-type.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-project-type.mjs","sources":["../../../../shared/utils/get-project-type.ts"],"sourcesContent":["type ProjectType = 'Community' | 'Growth' | 'Enterprise';\n\n/**\n * Resolves the plan label from the license information returned by the\n * `/admin/project-type` endpoint (browser) or `strapi.ee` (server).\n *\n * This labels the edition that is running: it follows `isEE`, so a lapsed license reads\n * Community. Browser tracking and NPS report this label and the startup table applies the same\n * rule; server metrics also label the running edition, but never say Growth. For the plan a\n * license was issued for, lapsed or not (the Plan card, the Support card and the debug dump),\n * use `getLicensedPlan` below.\n *\n * The label is intentionally limited to three values:\n * - `Community` when there is no license (Community Edition).\n * - `Growth` when the licensed plan price id contains \"growth\".\n * - `Enterprise` for any other licensed plan.\n */\nconst getProjectType = ({\n isEE,\n planPriceId,\n}: {\n isEE: boolean;\n planPriceId?: string;\n}): ProjectType => {\n if (!isEE) {\n return 'Community';\n }\n\n if (planPriceId?.toLowerCase().includes('growth')) {\n return 'Growth';\n }\n\n return 'Enterprise';\n};\n\n// The statuses a license can be in once it has been read. `none` (never licensed) is left out\n// on purpose, and so is a missing status, which a mixed-version install can produce.\nconst LICENSED_STATUSES: readonly string[] = ['active', 'expired', 'unknown'];\n\n/**\n * Resolves the plan a license was issued for, including one that has lapsed or can no longer\n * be read, so Support links, the Plan card and the debug dump can still name it. It unlocks\n * nothing: feature gating stays on `isEE`.\n *\n * A plan is only reported when a type was verified at some point (live, or retained once the\n * license lapsed) AND the status is licensed. A corrupt license file sets `unknown` before any\n * type is stored, and with no price id `getProjectType` would fall through to Enterprise, so\n * both conditions are required. Every caller that derives a plan should go through here.\n */\nconst getLicensedPlan = ({\n licenseStatus,\n type,\n planPriceId,\n}: {\n licenseStatus: string | null | undefined;\n type: string | null | undefined;\n planPriceId?: string | null;\n}): ProjectType =>\n getProjectType({\n isEE: Boolean(type) && LICENSED_STATUSES.includes(licenseStatus ?? ''),\n planPriceId: planPriceId ?? undefined,\n });\n\nexport { getLicensedPlan, getProjectType };\nexport type { ProjectType };\n"],"names":["getProjectType","isEE","planPriceId","toLowerCase","includes","LICENSED_STATUSES","getLicensedPlan","licenseStatus","type","Boolean","undefined"],"mappings":"AAEA;;;;;;;;;;;;;;AAcC,UACKA,cAAAA,GAAiB,CAAC,EACtBC,IAAI,EACJC,WAAW,EAIZ,GAAA;AACC,IAAA,IAAI,CAACD,IAAAA,EAAM;QACT,OAAO,WAAA;AACT,IAAA;IAEA,IAAIC,WAAAA,EAAaC,WAAAA,EAAAA,CAAcC,QAAAA,CAAS,QAAA,CAAA,EAAW;QACjD,OAAO,QAAA;AACT,IAAA;IAEA,OAAO,YAAA;AACT;AAEA;AACA;AACA,MAAMC,iBAAAA,GAAuC;AAAC,IAAA,QAAA;AAAU,IAAA,SAAA;AAAW,IAAA;AAAU,CAAA;AAE7E;;;;;;;;;IAUA,MAAMC,eAAAA,GAAkB,CAAC,EACvBC,aAAa,EACbC,IAAI,EACJN,WAAW,EAKZ,GACCF,cAAAA,CAAe;AACbC,QAAAA,IAAAA,EAAMQ,OAAAA,CAAQD,IAAAA,CAAAA,IAASH,iBAAAA,CAAkBD,QAAQ,CAACG,aAAAA,IAAiB,EAAA,CAAA;AACnEL,QAAAA,WAAAA,EAAaA,WAAAA,IAAeQ;AAC9B,KAAA;;;;"}
|