@logto/core-kit 2.8.0 → 2.10.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.
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ export declare const customUiCspDirectives: readonly ["scriptSrc", "connectSrc"];
3
+ export type CustomUiCspDirective = (typeof customUiCspDirectives)[number];
4
+ export type CustomUiCsp = {
5
+ scriptSrc?: string[];
6
+ connectSrc?: string[];
7
+ };
8
+ export declare const customUiCspGuard: z.ZodObject<{
9
+ scriptSrc: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
10
+ connectSrc: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
11
+ }, "strict", z.ZodTypeAny, {
12
+ scriptSrc?: string[] | undefined;
13
+ connectSrc?: string[] | undefined;
14
+ }, {
15
+ scriptSrc?: string[] | undefined;
16
+ connectSrc?: string[] | undefined;
17
+ }>;
18
+ export declare enum CustomUiCspSourceValidationErrorCode {
19
+ EmptySource = "empty_source",
20
+ SemicolonNotAllowed = "semicolon_not_allowed",
21
+ CspKeywordNotSupported = "csp_keyword_not_supported",
22
+ MalformedUrl = "malformed_url",
23
+ DisallowedUrlParts = "disallowed_url_parts",
24
+ UnsupportedScheme = "unsupported_scheme",
25
+ MalformedWildcardHost = "malformed_wildcard_host",
26
+ MalformedHost = "malformed_host"
27
+ }
28
+ export type CustomUiCspSourceValidationError = {
29
+ readonly directive: CustomUiCspDirective;
30
+ readonly source: string;
31
+ readonly code: CustomUiCspSourceValidationErrorCode;
32
+ };
33
+ export type CustomUiCspSourceValidationResult = {
34
+ readonly isValid: true;
35
+ readonly value: string;
36
+ } | {
37
+ readonly isValid: false;
38
+ readonly code: CustomUiCspSourceValidationErrorCode;
39
+ };
40
+ type CustomUiCspValidationOptions = {
41
+ readonly isProduction?: boolean;
42
+ };
43
+ export type NormalizeCustomUiCspResult = {
44
+ readonly customUiCsp: CustomUiCsp;
45
+ readonly errors: CustomUiCspSourceValidationError[];
46
+ };
47
+ export declare const normalizeCustomUiCspSourceExpression: (directive: CustomUiCspDirective, rawSource: string, options?: CustomUiCspValidationOptions) => CustomUiCspSourceValidationResult;
48
+ export declare const normalizeCustomUiCsp: (customUiCsp: CustomUiCsp, options?: CustomUiCspValidationOptions) => NormalizeCustomUiCspResult;
49
+ export declare const hasCustomUiCspSources: (customUiCsp?: CustomUiCsp) => boolean;
50
+ export {};
@@ -0,0 +1,121 @@
1
+ import { z } from 'zod';
2
+ export const customUiCspDirectives = Object.freeze(['scriptSrc', 'connectSrc']);
3
+ export const customUiCspGuard = z
4
+ .object({
5
+ scriptSrc: z.string().array().optional(),
6
+ connectSrc: z.string().array().optional(),
7
+ })
8
+ .strict();
9
+ export var CustomUiCspSourceValidationErrorCode;
10
+ (function (CustomUiCspSourceValidationErrorCode) {
11
+ CustomUiCspSourceValidationErrorCode["EmptySource"] = "empty_source";
12
+ CustomUiCspSourceValidationErrorCode["SemicolonNotAllowed"] = "semicolon_not_allowed";
13
+ CustomUiCspSourceValidationErrorCode["CspKeywordNotSupported"] = "csp_keyword_not_supported";
14
+ CustomUiCspSourceValidationErrorCode["MalformedUrl"] = "malformed_url";
15
+ CustomUiCspSourceValidationErrorCode["DisallowedUrlParts"] = "disallowed_url_parts";
16
+ CustomUiCspSourceValidationErrorCode["UnsupportedScheme"] = "unsupported_scheme";
17
+ CustomUiCspSourceValidationErrorCode["MalformedWildcardHost"] = "malformed_wildcard_host";
18
+ CustomUiCspSourceValidationErrorCode["MalformedHost"] = "malformed_host";
19
+ })(CustomUiCspSourceValidationErrorCode || (CustomUiCspSourceValidationErrorCode = {}));
20
+ const validHostLabelRegEx = /^[\da-z](?:[\da-z-]{0,61}[\da-z])?$/;
21
+ const isValidHostLabel = (label) => validHostLabelRegEx.test(label);
22
+ const validateHostname = (hostname) => {
23
+ if (hostname === 'localhost') {
24
+ return;
25
+ }
26
+ const labels = hostname.split('.');
27
+ const wildcardCount = labels.filter((label) => label === '*').length;
28
+ if (wildcardCount > 0) {
29
+ return labels[0] === '*' &&
30
+ wildcardCount === 1 &&
31
+ labels.length >= 3 &&
32
+ labels.slice(1).every((label) => isValidHostLabel(label))
33
+ ? undefined
34
+ : CustomUiCspSourceValidationErrorCode.MalformedWildcardHost;
35
+ }
36
+ return labels.length >= 2 && labels.every((label) => isValidHostLabel(label))
37
+ ? undefined
38
+ : CustomUiCspSourceValidationErrorCode.MalformedHost;
39
+ };
40
+ const validateScheme = (directive, url, { isProduction = false }) => {
41
+ const isLocalhostHttpSource = !isProduction && url.protocol === 'http:' && url.hostname === 'localhost' && url.port;
42
+ if (isLocalhostHttpSource) {
43
+ return;
44
+ }
45
+ const allowedSchemes = directive === 'connectSrc' ? ['https:', 'wss:'] : ['https:'];
46
+ return allowedSchemes.includes(url.protocol)
47
+ ? undefined
48
+ : CustomUiCspSourceValidationErrorCode.UnsupportedScheme;
49
+ };
50
+ const getInvalidPlainSourceCode = (source) => {
51
+ if (!source) {
52
+ return CustomUiCspSourceValidationErrorCode.EmptySource;
53
+ }
54
+ if (source.includes(';')) {
55
+ return CustomUiCspSourceValidationErrorCode.SemicolonNotAllowed;
56
+ }
57
+ return source.includes("'")
58
+ ? CustomUiCspSourceValidationErrorCode.CspKeywordNotSupported
59
+ : undefined;
60
+ };
61
+ const hasDisallowedUrlParts = (url) => Boolean(url.username || url.password || url.search || url.hash);
62
+ const normalizeParsedSourceExpression = (directive, url, options) => {
63
+ if (hasDisallowedUrlParts(url)) {
64
+ return {
65
+ isValid: false,
66
+ code: CustomUiCspSourceValidationErrorCode.DisallowedUrlParts,
67
+ };
68
+ }
69
+ const invalidSchemeCode = validateScheme(directive, url, options ?? {});
70
+ if (invalidSchemeCode) {
71
+ return { isValid: false, code: invalidSchemeCode };
72
+ }
73
+ const invalidHostCode = validateHostname(url.hostname);
74
+ if (invalidHostCode) {
75
+ return { isValid: false, code: invalidHostCode };
76
+ }
77
+ return {
78
+ isValid: true,
79
+ value: `${url.protocol}//${url.host}${url.pathname === '/' ? '' : url.pathname}`,
80
+ };
81
+ };
82
+ export const normalizeCustomUiCspSourceExpression = (directive, rawSource, options) => {
83
+ const source = rawSource.trim();
84
+ const invalidSourceCode = getInvalidPlainSourceCode(source);
85
+ if (invalidSourceCode) {
86
+ return { isValid: false, code: invalidSourceCode };
87
+ }
88
+ try {
89
+ return normalizeParsedSourceExpression(directive, new URL(source), options);
90
+ }
91
+ catch {
92
+ return { isValid: false, code: CustomUiCspSourceValidationErrorCode.MalformedUrl };
93
+ }
94
+ };
95
+ const normalizeDirectiveSources = (directive, sources, options) => {
96
+ const results = sources.map((source) => ({
97
+ source,
98
+ result: normalizeCustomUiCspSourceExpression(directive, source, options),
99
+ }));
100
+ return {
101
+ sources: [...new Set(results.flatMap(({ result }) => (result.isValid ? [result.value] : [])))],
102
+ errors: results.flatMap(({ source, result }) => result.isValid ? [] : [{ directive, source, code: result.code }]),
103
+ };
104
+ };
105
+ export const normalizeCustomUiCsp = (customUiCsp, options) => customUiCspDirectives.reduce(({ customUiCsp: normalizedCustomUiCsp, errors }, directive) => {
106
+ const sources = customUiCsp[directive];
107
+ if (!sources?.length) {
108
+ return { customUiCsp: normalizedCustomUiCsp, errors };
109
+ }
110
+ const result = normalizeDirectiveSources(directive, sources, options);
111
+ return {
112
+ customUiCsp: result.sources.length > 0
113
+ ? {
114
+ ...normalizedCustomUiCsp,
115
+ [directive]: result.sources,
116
+ }
117
+ : normalizedCustomUiCsp,
118
+ errors: [...errors, ...result.errors],
119
+ };
120
+ }, { customUiCsp: {}, errors: [] });
121
+ export const hasCustomUiCspSources = (customUiCsp) => Boolean(customUiCsp && customUiCspDirectives.some((directive) => customUiCsp[directive]?.length));
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './utils/index.js';
2
+ export * from './custom-ui-csp.js';
2
3
  export * from './regex.js';
3
4
  export * from './openid.js';
4
5
  export * from './models/index.js';
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './utils/index.js';
2
+ export * from './custom-ui-csp.js';
2
3
  export * from './regex.js';
3
4
  export * from './openid.js';
4
5
  export * from './models/index.js';
package/lib/openid.d.ts CHANGED
@@ -122,6 +122,7 @@ export declare const idTokenClaims: Readonly<Record<UserScope, UserClaim[]>>;
122
122
  * @see {@link userClaims} for all possible claims (used by userinfo endpoint).
123
123
  */
124
124
  export declare const extendedIdTokenClaimsByScope: Readonly<Partial<Record<UserScope, ExtendedIdTokenClaim[]>>>;
125
+ export declare const protectedAppAdditionalScopes: readonly [UserScope.CustomData, UserScope.Identities, UserScope.Roles, UserScope.Organizations, UserScope.OrganizationRoles];
125
126
  /**
126
127
  * All possible claims for each scope, combining base ID token claims and extended claims.
127
128
  *
package/lib/openid.js CHANGED
@@ -194,6 +194,13 @@ export const extendedIdTokenClaimsByScope = Object.freeze({
194
194
  [UserScope.Organizations]: ['organizations', 'organization_data'],
195
195
  [UserScope.OrganizationRoles]: ['organization_roles'],
196
196
  });
197
+ export const protectedAppAdditionalScopes = [
198
+ UserScope.CustomData,
199
+ UserScope.Identities,
200
+ UserScope.Roles,
201
+ UserScope.Organizations,
202
+ UserScope.OrganizationRoles,
203
+ ];
197
204
  /**
198
205
  * All possible claims for each scope, combining base ID token claims and extended claims.
199
206
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/core-kit",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
4
4
  "author": "Silverhand Inc. <contact@silverhand.io>",
5
5
  "homepage": "https://github.com/logto-io/toolkit#readme",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "@silverhand/essentials": "^2.9.1",
37
37
  "color": "^4.2.3",
38
38
  "@logto/language-kit": "^1.3.0",
39
- "@logto/shared": "^3.3.1"
39
+ "@logto/shared": "^3.4.0"
40
40
  },
41
41
  "optionalDependencies": {
42
42
  "zod": "3.24.3"
@@ -166,6 +166,7 @@
166
166
  --color-specific-focused-inside: var(--color-primary-30);
167
167
  --color-specific-focused-outside: var(--color-primary-40);
168
168
  --color-specific-button-icon: rgba(255, 255, 255, 70%); // 70% static white
169
+ --color-overlay-primary-subtle: rgba(93, 52, 242, 4%); // 4% Primary-40
169
170
  --color-overlay-primary-hover: rgba(93, 52, 242, 8%); // 8% Primary-40
170
171
  --color-overlay-primary-pressed: rgba(93, 52, 242, 12%); // 12% Primary-40
171
172
  --color-function-n-overlay-primary-focused: rgba(93, 52, 242, 16%); // 16% Primary-40
@@ -390,6 +391,7 @@
390
391
  --color-specific-focused-inside: var(--color-primary-40);
391
392
  --color-specific-focused-outside: rgba(#cabeff, 32%); // 32% Primary-40
392
393
  --color-specific-button-icon: rgba(255, 255, 255, 60%); // 60% static white
394
+ --color-overlay-primary-subtle: rgba(202, 190, 255, 4%); // 4% Primary-40
393
395
  --color-overlay-primary-hover: rgba(202, 190, 255, 8%); // 8% Primary-40
394
396
  --color-overlay-primary-pressed: rgba(202, 190, 255, 12%); // 12% Primary-40
395
397
  --color-function-n-overlay-primary-focused: rgba(202, 190, 255, 16%); // 16% Primary-40