veryfront 0.1.1213 → 0.1.1214
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/esm/deno.js +1 -1
- package/esm/src/agent/runtime/index.d.ts.map +1 -1
- package/esm/src/agent/runtime/index.js +2 -6
- package/esm/src/agent/runtime/skill-policy-enforcement.d.ts +1 -1
- package/esm/src/agent/runtime/skill-policy-enforcement.d.ts.map +1 -1
- package/esm/src/agent/runtime/skill-policy-enforcement.js +1 -8
- package/esm/src/html/hydration-script-builder/hydration-runtime.generated.js +1 -1
- package/esm/src/platform/adapters/index.d.ts +25 -0
- package/esm/src/platform/adapters/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/index.js +23 -0
- package/esm/src/platform/adapters/security/index.d.ts +8 -0
- package/esm/src/platform/adapters/security/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/security/index.js +7 -0
- package/esm/src/platform/adapters/token/factory.d.ts +10 -0
- package/esm/src/platform/adapters/token/factory.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/factory.js +79 -0
- package/esm/src/platform/adapters/token/index.d.ts +9 -0
- package/esm/src/platform/adapters/token/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/index.js +8 -0
- package/esm/src/platform/adapters/token/integration.d.ts +6 -0
- package/esm/src/platform/adapters/token/integration.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/integration.js +61 -0
- package/esm/src/platform/adapters/token/veryfront/adapter.d.ts +22 -0
- package/esm/src/platform/adapters/token/veryfront/adapter.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/adapter.js +88 -0
- package/esm/src/platform/adapters/token/veryfront/api-client.d.ts +15 -0
- package/esm/src/platform/adapters/token/veryfront/api-client.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/api-client.js +212 -0
- package/esm/src/platform/adapters/token/veryfront/index.d.ts +10 -0
- package/esm/src/platform/adapters/token/veryfront/index.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/index.js +9 -0
- package/esm/src/platform/adapters/token/veryfront/memory-adapter.d.ts +20 -0
- package/esm/src/platform/adapters/token/veryfront/memory-adapter.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/memory-adapter.js +52 -0
- package/esm/src/platform/adapters/token/veryfront/types.d.ts +86 -0
- package/esm/src/platform/adapters/token/veryfront/types.d.ts.map +1 -0
- package/esm/src/platform/adapters/token/veryfront/types.js +81 -0
- package/esm/src/platform/compat/http/pinned-fetch.d.ts +1 -1
- package/esm/src/security/http/config.d.ts +5 -0
- package/esm/src/security/http/config.d.ts.map +1 -1
- package/esm/src/security/http/config.js +16 -1
- package/esm/src/security/http/derived-csp-cache.d.ts +33 -0
- package/esm/src/security/http/derived-csp-cache.d.ts.map +1 -0
- package/esm/src/security/http/derived-csp-cache.js +101 -0
- package/esm/src/security/http/derived-csp-origins.d.ts +54 -0
- package/esm/src/security/http/derived-csp-origins.d.ts.map +1 -0
- package/esm/src/security/http/derived-csp-origins.js +156 -0
- package/esm/src/security/http/response/security-handler.d.ts.map +1 -1
- package/esm/src/security/http/response/security-handler.js +9 -2
- package/esm/src/security/sandbox/constants.d.ts +3 -0
- package/esm/src/security/sandbox/constants.d.ts.map +1 -0
- package/esm/src/security/sandbox/constants.js +2 -0
- package/esm/src/security/sandbox/deno-sandbox.d.ts +71 -0
- package/esm/src/security/sandbox/deno-sandbox.d.ts.map +1 -0
- package/esm/src/security/sandbox/deno-sandbox.js +184 -0
- package/esm/src/security/sandbox/permission-system.d.ts +11 -0
- package/esm/src/security/sandbox/permission-system.d.ts.map +1 -0
- package/esm/src/security/sandbox/permission-system.js +85 -0
- package/esm/src/server/runtime-handler/project-runtime-context.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/project-runtime-context.js +97 -1
- package/esm/src/types/server.d.ts +10 -0
- package/esm/src/types/server.d.ts.map +1 -1
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/esm/src/workflow/api.d.ts +2 -2
- package/package.json +8 -8
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Storage Adapter Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface for token storage backends.
|
|
5
|
+
* Tokens are encrypted client-side before being sent to the backend.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEFAULT_TOKEN_STORAGE_REQUEST_TIMEOUT_MS = 30000;
|
|
8
|
+
/**
|
|
9
|
+
* Token storage adapter interface
|
|
10
|
+
*
|
|
11
|
+
* Simple key-value interface for storing encrypted tokens.
|
|
12
|
+
* Keys are formatted as "{userId}:{serviceId}" (e.g., "user123:gmail").
|
|
13
|
+
* Values are encrypted token blobs (client encrypts before sending).
|
|
14
|
+
*/
|
|
15
|
+
export interface TokenStorageAdapter {
|
|
16
|
+
/** Get encrypted token by key */
|
|
17
|
+
get(key: string): Promise<string | null>;
|
|
18
|
+
/** Set encrypted token by key (upsert) */
|
|
19
|
+
set(key: string, value: string): Promise<void>;
|
|
20
|
+
/** Delete token by key (idempotent) */
|
|
21
|
+
delete(key: string): Promise<void>;
|
|
22
|
+
/** List all keys with optional prefix filter */
|
|
23
|
+
list?(prefix?: string): Promise<string[]>;
|
|
24
|
+
/** Initialize the adapter (e.g., verify connection) */
|
|
25
|
+
initialize?(): Promise<void>;
|
|
26
|
+
/** Cleanup resources */
|
|
27
|
+
dispose?(): void;
|
|
28
|
+
}
|
|
29
|
+
/** Veryfront Cloud token-storage options. */
|
|
30
|
+
export interface VeryfrontTokenStorageOptions {
|
|
31
|
+
/** API token for authentication */
|
|
32
|
+
apiToken: string;
|
|
33
|
+
/** Project slug */
|
|
34
|
+
projectSlug: string;
|
|
35
|
+
/** API base URL (defaults to production) */
|
|
36
|
+
apiBaseUrl?: string;
|
|
37
|
+
/** Per-request timeout in milliseconds. Defaults to 30000ms. */
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
/** Retry configuration */
|
|
40
|
+
retry?: {
|
|
41
|
+
/** Retries after the initial request, from 0 through 9. */
|
|
42
|
+
maxRetries?: number;
|
|
43
|
+
/** Initial retry delay in whole milliseconds. */
|
|
44
|
+
initialDelay?: number;
|
|
45
|
+
/** Maximum retry delay in whole milliseconds. */
|
|
46
|
+
maxDelay?: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for token storage adapters.
|
|
51
|
+
*
|
|
52
|
+
* The discriminator is optional only for the empty/default memory config.
|
|
53
|
+
* Cloud options can never be combined with, or silently downgraded to, the
|
|
54
|
+
* in-memory adapter.
|
|
55
|
+
*/
|
|
56
|
+
export type TokenStorageAdapterConfig = {
|
|
57
|
+
type?: "memory";
|
|
58
|
+
veryfront?: never;
|
|
59
|
+
} | {
|
|
60
|
+
type: "veryfront-api";
|
|
61
|
+
veryfront: VeryfrontTokenStorageOptions;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Internal config with defaults applied
|
|
65
|
+
*/
|
|
66
|
+
export interface VeryfrontTokenConfig {
|
|
67
|
+
apiBaseUrl: string;
|
|
68
|
+
apiToken: string;
|
|
69
|
+
projectSlug: string;
|
|
70
|
+
/** Request timeout in milliseconds. */
|
|
71
|
+
timeoutMs: number;
|
|
72
|
+
retry: {
|
|
73
|
+
maxRetries: number;
|
|
74
|
+
initialDelay: number;
|
|
75
|
+
maxDelay: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create verified config from adapter config
|
|
80
|
+
*/
|
|
81
|
+
export declare function createTokenConfig(config: TokenStorageAdapterConfig): VeryfrontTokenConfig;
|
|
82
|
+
export declare function assertTokenStorageKey(key: string): void;
|
|
83
|
+
export declare function assertTokenStoragePrefix(prefix: string | undefined): void;
|
|
84
|
+
export declare function assertTokenStorageValue(value: string): void;
|
|
85
|
+
export { TOKEN_STORAGE_ERROR } from "../../../../errors/index.js";
|
|
86
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/token/veryfront/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,eAAO,MAAM,wCAAwC,QAAS,CAAC;AAG/D;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,iCAAiC;IACjC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAEzC,0CAA0C;IAC1C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C,uCAAuC;IACvC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC,gDAAgD;IAChD,IAAI,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE1C,uDAAuD;IACvD,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B,wBAAwB;IACxB,OAAO,CAAC,IAAI,IAAI,CAAC;CAClB;AAED,6CAA6C;AAC7C,MAAM,WAAW,4BAA4B;IAC3C,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0BAA0B;IAC1B,KAAK,CAAC,EAAE;QACN,2DAA2D;QAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,iDAAiD;QACjD,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,iDAAiD;QACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GACjC;IAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;IAAC,SAAS,CAAC,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,4BAA4B,CAAA;CAAE,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAgBD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,yBAAyB,GAAG,oBAAoB,CAiCzF;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGzE;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAI3D;AA8BD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Storage Adapter Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface for token storage backends.
|
|
5
|
+
* Tokens are encrypted client-side before being sent to the backend.
|
|
6
|
+
*/
|
|
7
|
+
import { createError, toError } from "../../../../errors/index.js";
|
|
8
|
+
import { normalizeVeryfrontApiRetryConfig } from "../../../../utils/config-resource-limits.js";
|
|
9
|
+
import { normalizeTimerDurationMs } from "../../../../utils/timer.js";
|
|
10
|
+
export const DEFAULT_TOKEN_STORAGE_REQUEST_TIMEOUT_MS = 30_000;
|
|
11
|
+
const MAX_TOKEN_STORAGE_KEY_CODE_UNITS = 4_096;
|
|
12
|
+
function requireVeryfrontConfig(config) {
|
|
13
|
+
const veryfront = config.veryfront;
|
|
14
|
+
if (veryfront)
|
|
15
|
+
return veryfront;
|
|
16
|
+
throw toError(createError({
|
|
17
|
+
type: "config",
|
|
18
|
+
message: "Veryfront token adapter requires veryfront configuration",
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create verified config from adapter config
|
|
23
|
+
*/
|
|
24
|
+
export function createTokenConfig(config) {
|
|
25
|
+
const veryfront = requireVeryfrontConfig(config);
|
|
26
|
+
const apiToken = veryfront.apiToken;
|
|
27
|
+
if (!isNonBlankString(apiToken)) {
|
|
28
|
+
throw toError(createError({
|
|
29
|
+
type: "config",
|
|
30
|
+
message: "Veryfront token adapter requires apiToken",
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
const projectSlug = veryfront.projectSlug;
|
|
34
|
+
if (!isNonBlankString(projectSlug)) {
|
|
35
|
+
throw toError(createError({
|
|
36
|
+
type: "config",
|
|
37
|
+
message: "Veryfront token adapter requires projectSlug",
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
apiBaseUrl: veryfront.apiBaseUrl ?? "https://api.veryfront.com",
|
|
42
|
+
apiToken,
|
|
43
|
+
projectSlug,
|
|
44
|
+
timeoutMs: normalizeTimerDurationMs(veryfront.timeoutMs ?? DEFAULT_TOKEN_STORAGE_REQUEST_TIMEOUT_MS, "Token storage timeoutMs"),
|
|
45
|
+
retry: normalizeVeryfrontApiRetryConfig(veryfront.retry),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function assertTokenStorageKey(key) {
|
|
49
|
+
assertBoundedTokenStorageIdentifier(key, "key", false);
|
|
50
|
+
}
|
|
51
|
+
export function assertTokenStoragePrefix(prefix) {
|
|
52
|
+
if (prefix === undefined)
|
|
53
|
+
return;
|
|
54
|
+
assertBoundedTokenStorageIdentifier(prefix, "prefix", true);
|
|
55
|
+
}
|
|
56
|
+
export function assertTokenStorageValue(value) {
|
|
57
|
+
if (typeof value !== "string") {
|
|
58
|
+
throw new TypeError("Token storage value must be a string");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isNonBlankString(value) {
|
|
62
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
63
|
+
}
|
|
64
|
+
function assertBoundedTokenStorageIdentifier(value, label, allowEmpty) {
|
|
65
|
+
if (typeof value !== "string") {
|
|
66
|
+
throw new TypeError(`Token storage ${label} must be a string`);
|
|
67
|
+
}
|
|
68
|
+
if (!allowEmpty && value.length === 0) {
|
|
69
|
+
throw new TypeError("Token storage key must be non-empty");
|
|
70
|
+
}
|
|
71
|
+
if (value.length > MAX_TOKEN_STORAGE_KEY_CODE_UNITS) {
|
|
72
|
+
throw new RangeError(`Token storage ${label} must not exceed ${MAX_TOKEN_STORAGE_KEY_CODE_UNITS} code units`);
|
|
73
|
+
}
|
|
74
|
+
for (let index = 0; index < value.length; index++) {
|
|
75
|
+
const codeUnit = value.charCodeAt(index);
|
|
76
|
+
if (codeUnit <= 0x1f || codeUnit === 0x7f) {
|
|
77
|
+
throw new TypeError(`Token storage ${label} must not contain control characters`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export { TOKEN_STORAGE_ERROR } from "../../../../errors/index.js";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Client identity for guarded egress, standing in for the runtime-supplied
|
|
8
8
|
* `user-agent` (`node`, `Deno/x.y.z`) that this transport cannot inherit.
|
|
9
9
|
*/
|
|
10
|
-
export declare const DEFAULT_OUTBOUND_USER_AGENT = "veryfront/0.1.
|
|
10
|
+
export declare const DEFAULT_OUTBOUND_USER_AGENT = "veryfront/0.1.1214";
|
|
11
11
|
/**
|
|
12
12
|
* Fill in the request headers a plain `fetch` attaches on its own, leaving any
|
|
13
13
|
* the caller set untouched. This transport talks to `node:http` directly, so
|
|
@@ -11,6 +11,11 @@ export interface DeriveSecurityContextOptions {
|
|
|
11
11
|
* classification may override it explicitly.
|
|
12
12
|
*/
|
|
13
13
|
productionDefaults?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Origins derived from the project's released source. Platform-supplied;
|
|
16
|
+
* anything a project config carries under this name is discarded.
|
|
17
|
+
*/
|
|
18
|
+
derivedCsp?: SecurityConfig["derivedCsp"];
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* Check a standalone security configuration against the canonical project
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/src/security/http/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQ7D,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,cAAc,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../src/src/security/http/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQ7D,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,cAAc,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;CAC3C;AA4LD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,cAAc,CAO/E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAIhC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,CAAC,EAAE,eAAe,EACrB,OAAO,GAAE,4BAAiC,GACzC,sBAAsB,CA4BxB;AAED,qBAAa,oBAAoB;IAM7B,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,cAAc,CAAC;IACvB,OAAO,CAAC,iBAAiB;IAR3B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAA8B;gBAGvC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,cAAc,EACvB,cAAc,CAAC,EAAE,eAAe,YAAA,EAChC,iBAAiB,UAAQ;IAG7B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;YAqBrB,IAAI;IAKlB,OAAO,CAAC,WAAW;IAiBnB,iBAAiB,IAAI,cAAc,GAAG,IAAI;IAI1C,aAAa,IAAI,cAAc,CAAC,MAAM,CAAC;IAIvC,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,GAAE,MAAwB,GAAG,MAAM;IAIjE,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;CAQpE"}
|
|
@@ -160,8 +160,12 @@ function readProductionDefaults(options) {
|
|
|
160
160
|
const prototype = Object.getPrototypeOf(options);
|
|
161
161
|
if (prototype !== Object.prototype && prototype !== null)
|
|
162
162
|
return invalidSecurityConfig();
|
|
163
|
+
// Allowlisted rather than ignored: this object reaches a security decision,
|
|
164
|
+
// so an unrecognised key is a caller mistake worth failing on, not a
|
|
165
|
+
// silently discarded one.
|
|
166
|
+
const allowed = new Set(["productionDefaults", "derivedCsp"]);
|
|
163
167
|
const keys = Reflect.ownKeys(options);
|
|
164
|
-
if (keys.some((key) => key
|
|
168
|
+
if (keys.some((key) => !allowed.has(key)))
|
|
165
169
|
return invalidSecurityConfig();
|
|
166
170
|
const descriptor = Object.getOwnPropertyDescriptor(options, "productionDefaults");
|
|
167
171
|
if (!descriptor)
|
|
@@ -228,6 +232,17 @@ export function deriveSecurityContext(cfg, options = {}) {
|
|
|
228
232
|
if (normalized.csrf === undefined && productionDefaults) {
|
|
229
233
|
normalized.csrf = true;
|
|
230
234
|
}
|
|
235
|
+
// Assigned unconditionally, never merged. `SecurityConfig` carries an index
|
|
236
|
+
// signature, so the clone above would happily copy a `derivedCsp` a project
|
|
237
|
+
// wrote in its own config; overwriting here makes the platform the only
|
|
238
|
+
// author of this layer. Deleting when absent keeps a stale project value
|
|
239
|
+
// from surviving as the derived set.
|
|
240
|
+
if (options.derivedCsp && Object.keys(options.derivedCsp).length > 0) {
|
|
241
|
+
normalized.derivedCsp = options.derivedCsp;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
delete normalized.derivedCsp;
|
|
245
|
+
}
|
|
231
246
|
const securityConfig = Object.freeze(normalized);
|
|
232
247
|
return Object.freeze({
|
|
233
248
|
securityConfig,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-release cache for {@link deriveCspOriginsFromSource}.
|
|
3
|
+
*
|
|
4
|
+
* Derivation reads every source file a release pins, which is far too much work
|
|
5
|
+
* to repeat per response but exactly the right amount to do once per release:
|
|
6
|
+
* the input is immutable for a given content version, so the result is too.
|
|
7
|
+
*
|
|
8
|
+
* @module security/http/derived-csp-cache
|
|
9
|
+
*/
|
|
10
|
+
import { type DerivationSourceFile, type DerivedCspOrigins } from "./derived-csp-origins.js";
|
|
11
|
+
export interface DerivedCspLookup {
|
|
12
|
+
/** Project scope, so two projects on one pod cannot share an entry. */
|
|
13
|
+
readonly projectScope: string;
|
|
14
|
+
/**
|
|
15
|
+
* Content version, as `resolveStyleContentVersion` spells it. A branch
|
|
16
|
+
* version is stable while its content changes, which is why the caller passes
|
|
17
|
+
* a snapshot identity rather than a branch name where one is available.
|
|
18
|
+
*/
|
|
19
|
+
readonly contentVersion: string;
|
|
20
|
+
/** Reads the release's source files. Called only on a miss. */
|
|
21
|
+
readonly loadSourceFiles: () => Promise<readonly DerivationSourceFile[] | null>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Derived origins for a content version, computing them at most once.
|
|
25
|
+
*
|
|
26
|
+
* Failure is not fatal: a project whose sources cannot be read gets an empty
|
|
27
|
+
* derivation, which leaves it exactly where it was before this existed -- the
|
|
28
|
+
* platform floor plus whatever `security.csp` declares.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getDerivedCspOrigins(lookup: DerivedCspLookup): Promise<DerivedCspOrigins>;
|
|
31
|
+
/** @internal Test seam. */
|
|
32
|
+
export declare function __clearDerivedCspCacheForTests(): void;
|
|
33
|
+
//# sourceMappingURL=derived-csp-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-csp-cache.d.ts","sourceRoot":"","sources":["../../../../src/src/security/http/derived-csp-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EACL,KAAK,oBAAoB,EAEzB,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAuClC,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,eAAe,EAAE,MAAM,OAAO,CAAC,SAAS,oBAAoB,EAAE,GAAG,IAAI,CAAC,CAAC;CACjF;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,iBAAiB,CAAC,CAmB5B;AA+BD,2BAA2B;AAC3B,wBAAgB,8BAA8B,IAAI,IAAI,CAGrD"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-release cache for {@link deriveCspOriginsFromSource}.
|
|
3
|
+
*
|
|
4
|
+
* Derivation reads every source file a release pins, which is far too much work
|
|
5
|
+
* to repeat per response but exactly the right amount to do once per release:
|
|
6
|
+
* the input is immutable for a given content version, so the result is too.
|
|
7
|
+
*
|
|
8
|
+
* @module security/http/derived-csp-cache
|
|
9
|
+
*/
|
|
10
|
+
import { registerCache } from "../../utils/memory/index.js";
|
|
11
|
+
import { serverLogger } from "../../utils/logger/logger.js";
|
|
12
|
+
import { deriveCspOriginsFromSource, } from "./derived-csp-origins.js";
|
|
13
|
+
const logger = serverLogger.component("derived-csp");
|
|
14
|
+
/** Bounded so a fleet of projects cannot grow this without limit. */
|
|
15
|
+
const MAX_ENTRIES = 200;
|
|
16
|
+
const EMPTY = Object.freeze({});
|
|
17
|
+
const cache = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Derivations currently running, keyed exactly as the resolved cache is.
|
|
20
|
+
*
|
|
21
|
+
* A key is coldest immediately after a release, when every pod serves that
|
|
22
|
+
* content version for the first time. Without this, each concurrent request for
|
|
23
|
+
* the same key reads and scans the whole source set; with it the first caller
|
|
24
|
+
* does the work and the rest await its result.
|
|
25
|
+
*/
|
|
26
|
+
const inFlight = new Map();
|
|
27
|
+
registerCache("derived-csp-origins", () => ({
|
|
28
|
+
name: "derived-csp-origins",
|
|
29
|
+
entries: cache.size,
|
|
30
|
+
maxEntries: MAX_ENTRIES,
|
|
31
|
+
}));
|
|
32
|
+
function remember(key, value) {
|
|
33
|
+
// Insertion-ordered eviction: the oldest content version is the one least
|
|
34
|
+
// likely to still be serving traffic.
|
|
35
|
+
while (cache.size >= MAX_ENTRIES) {
|
|
36
|
+
const oldest = cache.keys().next().value;
|
|
37
|
+
if (oldest === undefined)
|
|
38
|
+
break;
|
|
39
|
+
cache.delete(oldest);
|
|
40
|
+
}
|
|
41
|
+
cache.set(key, value);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Derived origins for a content version, computing them at most once.
|
|
46
|
+
*
|
|
47
|
+
* Failure is not fatal: a project whose sources cannot be read gets an empty
|
|
48
|
+
* derivation, which leaves it exactly where it was before this existed -- the
|
|
49
|
+
* platform floor plus whatever `security.csp` declares.
|
|
50
|
+
*/
|
|
51
|
+
export function getDerivedCspOrigins(lookup) {
|
|
52
|
+
// NUL separates the two components because neither can contain one, so no
|
|
53
|
+
// scope/version pair can collide with another by concatenation. Written as an
|
|
54
|
+
// escape rather than a raw byte: a literal NUL in the source makes the file
|
|
55
|
+
// binary to grep and friends.
|
|
56
|
+
const key = `${lookup.projectScope}\u0000${lookup.contentVersion}`;
|
|
57
|
+
const cached = cache.get(key);
|
|
58
|
+
if (cached)
|
|
59
|
+
return Promise.resolve(cached);
|
|
60
|
+
const pending = inFlight.get(key);
|
|
61
|
+
if (pending)
|
|
62
|
+
return pending;
|
|
63
|
+
const derivation = deriveOnce(key, lookup);
|
|
64
|
+
inFlight.set(key, derivation);
|
|
65
|
+
return derivation.finally(() => {
|
|
66
|
+
// Clear only our own entry: after an eviction a later call may already have
|
|
67
|
+
// started a fresh derivation under the same key.
|
|
68
|
+
if (inFlight.get(key) === derivation)
|
|
69
|
+
inFlight.delete(key);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function deriveOnce(key, lookup) {
|
|
73
|
+
let files;
|
|
74
|
+
try {
|
|
75
|
+
files = await lookup.loadSourceFiles();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
logger.debug("Could not read sources for CSP derivation", {
|
|
79
|
+
projectScope: lookup.projectScope,
|
|
80
|
+
error: error instanceof Error ? error.message : String(error),
|
|
81
|
+
});
|
|
82
|
+
return remember(key, EMPTY);
|
|
83
|
+
}
|
|
84
|
+
if (!files || files.length === 0)
|
|
85
|
+
return remember(key, EMPTY);
|
|
86
|
+
const derived = deriveCspOriginsFromSource(files);
|
|
87
|
+
const count = derived["img-src"]?.length ?? 0;
|
|
88
|
+
if (count > 0) {
|
|
89
|
+
logger.debug("Derived CSP origins from project source", {
|
|
90
|
+
projectScope: lookup.projectScope,
|
|
91
|
+
contentVersion: lookup.contentVersion,
|
|
92
|
+
originCount: count,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return remember(key, derived);
|
|
96
|
+
}
|
|
97
|
+
/** @internal Test seam. */
|
|
98
|
+
export function __clearDerivedCspCacheForTests() {
|
|
99
|
+
cache.clear();
|
|
100
|
+
inFlight.clear();
|
|
101
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a project's CSP origins from the source it publishes.
|
|
3
|
+
*
|
|
4
|
+
* The platform floor lists what the *platform* emits. Anything a project loads
|
|
5
|
+
* from elsewhere -- a stock-photo host, its own asset CDN, a video origin --
|
|
6
|
+
* has to be admitted separately, and until now the only way to do that was
|
|
7
|
+
* `security.csp`. That made a working site depend on configuration the project
|
|
8
|
+
* had no reason to know it needed: an audit of the hosted fleet found ~100
|
|
9
|
+
* projects loading blocked assets and two that had declared anything.
|
|
10
|
+
*
|
|
11
|
+
* This module closes that gap by reading the origins out of the project's own
|
|
12
|
+
* released source, so `security.csp` becomes an override for what static
|
|
13
|
+
* analysis cannot see rather than a prerequisite for a site that works.
|
|
14
|
+
*
|
|
15
|
+
* Two properties make that safe rather than merely convenient:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Only passive-content directives are derived.** `img-src`, `media-src`
|
|
18
|
+
* and `font-src` decide whether a logo renders. `script-src` and
|
|
19
|
+
* `connect-src` decide whether a third party can execute code in the page
|
|
20
|
+
* or receive data from it. Deriving those would mean anyone able to
|
|
21
|
+
* influence source -- a compromised dependency, a merged pull request,
|
|
22
|
+
* CMS-authored MDX -- could grant themselves execution or an exfiltration
|
|
23
|
+
* endpoint, collapsing two independent barriers into one. Every project in
|
|
24
|
+
* the audit was fixed by passive directives alone.
|
|
25
|
+
*
|
|
26
|
+
* 2. **Origins come from an immutable release.** Derivation runs over the
|
|
27
|
+
* source a release pins, not over live content, so a script injected at
|
|
28
|
+
* runtime cannot extend the allowlist it is subject to.
|
|
29
|
+
*
|
|
30
|
+
* Derivation is deliberately incomplete. A URL assembled at runtime -- a
|
|
31
|
+
* template literal, a CMS field, an environment variable -- is invisible here,
|
|
32
|
+
* and those projects still declare `security.csp`. Under-deriving costs a
|
|
33
|
+
* broken image; over-deriving costs the policy its meaning.
|
|
34
|
+
*
|
|
35
|
+
* @module security/http/derived-csp-origins
|
|
36
|
+
*/
|
|
37
|
+
/** Directives this module is permitted to contribute to. */
|
|
38
|
+
export declare const DERIVABLE_CSP_DIRECTIVES: readonly ["img-src", "media-src", "font-src"];
|
|
39
|
+
export type DerivableCspDirective = (typeof DERIVABLE_CSP_DIRECTIVES)[number];
|
|
40
|
+
export type DerivedCspOrigins = Readonly<Partial<Record<DerivableCspDirective, readonly string[]>>>;
|
|
41
|
+
/** A source file as the release stores it. */
|
|
42
|
+
export interface DerivationSourceFile {
|
|
43
|
+
readonly path: string;
|
|
44
|
+
readonly content?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Collect the passive-content origins a project's source references.
|
|
48
|
+
*
|
|
49
|
+
* Pure over its input: callers hand it the file set a release pins, and the
|
|
50
|
+
* result is stable for that set, which is what lets it be computed once per
|
|
51
|
+
* release rather than per request.
|
|
52
|
+
*/
|
|
53
|
+
export declare function deriveCspOriginsFromSource(files: readonly DerivationSourceFile[]): DerivedCspOrigins;
|
|
54
|
+
//# sourceMappingURL=derived-csp-origins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-csp-origins.d.ts","sourceRoot":"","sources":["../../../../src/src/security/http/derived-csp-origins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,4DAA4D;AAC5D,eAAO,MAAM,wBAAwB,+CAEpC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9E,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CACtC,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAC1D,CAAC;AAEF,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2ED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,SAAS,oBAAoB,EAAE,GACrC,iBAAiB,CAsCnB"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a project's CSP origins from the source it publishes.
|
|
3
|
+
*
|
|
4
|
+
* The platform floor lists what the *platform* emits. Anything a project loads
|
|
5
|
+
* from elsewhere -- a stock-photo host, its own asset CDN, a video origin --
|
|
6
|
+
* has to be admitted separately, and until now the only way to do that was
|
|
7
|
+
* `security.csp`. That made a working site depend on configuration the project
|
|
8
|
+
* had no reason to know it needed: an audit of the hosted fleet found ~100
|
|
9
|
+
* projects loading blocked assets and two that had declared anything.
|
|
10
|
+
*
|
|
11
|
+
* This module closes that gap by reading the origins out of the project's own
|
|
12
|
+
* released source, so `security.csp` becomes an override for what static
|
|
13
|
+
* analysis cannot see rather than a prerequisite for a site that works.
|
|
14
|
+
*
|
|
15
|
+
* Two properties make that safe rather than merely convenient:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Only passive-content directives are derived.** `img-src`, `media-src`
|
|
18
|
+
* and `font-src` decide whether a logo renders. `script-src` and
|
|
19
|
+
* `connect-src` decide whether a third party can execute code in the page
|
|
20
|
+
* or receive data from it. Deriving those would mean anyone able to
|
|
21
|
+
* influence source -- a compromised dependency, a merged pull request,
|
|
22
|
+
* CMS-authored MDX -- could grant themselves execution or an exfiltration
|
|
23
|
+
* endpoint, collapsing two independent barriers into one. Every project in
|
|
24
|
+
* the audit was fixed by passive directives alone.
|
|
25
|
+
*
|
|
26
|
+
* 2. **Origins come from an immutable release.** Derivation runs over the
|
|
27
|
+
* source a release pins, not over live content, so a script injected at
|
|
28
|
+
* runtime cannot extend the allowlist it is subject to.
|
|
29
|
+
*
|
|
30
|
+
* Derivation is deliberately incomplete. A URL assembled at runtime -- a
|
|
31
|
+
* template literal, a CMS field, an environment variable -- is invisible here,
|
|
32
|
+
* and those projects still declare `security.csp`. Under-deriving costs a
|
|
33
|
+
* broken image; over-deriving costs the policy its meaning.
|
|
34
|
+
*
|
|
35
|
+
* @module security/http/derived-csp-origins
|
|
36
|
+
*/
|
|
37
|
+
/** Directives this module is permitted to contribute to. */
|
|
38
|
+
export const DERIVABLE_CSP_DIRECTIVES = Object.freeze(["img-src", "media-src", "font-src"]);
|
|
39
|
+
/**
|
|
40
|
+
* Cap on how much of one file is scanned. A generated bundle or an inlined
|
|
41
|
+
* data blob can be megabytes, and scanning it yields nothing a hand-written
|
|
42
|
+
* source file would not.
|
|
43
|
+
*
|
|
44
|
+
* Measured in UTF-16 code units, matching `String.prototype.length`, because
|
|
45
|
+
* that is the unit the regex engine actually steps through -- 512K code units
|
|
46
|
+
* is the same amount of scanning whether they are ASCII or CJK. A UTF-8 byte
|
|
47
|
+
* cap would read as the more natural bound but would not correspond to the
|
|
48
|
+
* cost being bounded, and computing it means walking the string to decide how
|
|
49
|
+
* much of the string to walk.
|
|
50
|
+
*/
|
|
51
|
+
const MAX_SCANNED_CHARS_PER_FILE = 512 * 1024;
|
|
52
|
+
/**
|
|
53
|
+
* Upper bound on distinct origins, so a generated file cannot inflate the
|
|
54
|
+
* policy. Origins are ranked before the cap applies -- see
|
|
55
|
+
* {@link deriveCspOriginsFromSource}.
|
|
56
|
+
*/
|
|
57
|
+
const MAX_DERIVED_ORIGINS = 32;
|
|
58
|
+
/**
|
|
59
|
+
* Every absolute https URL in the source, whatever syntax surrounds it.
|
|
60
|
+
*
|
|
61
|
+
* An earlier version of this matched by position -- `<img src>`, `url()`,
|
|
62
|
+
* `poster` -- on the theory that knowing which element referenced a URL would
|
|
63
|
+
* let each origin land in exactly the right directive. Measured against real
|
|
64
|
+
* released content that recovers about a third of the references. The same
|
|
65
|
+
* project reaches its asset host through `<img src>`, a JSX `imageSrc` prop,
|
|
66
|
+
* YAML frontmatter, markdown ``, a bare string in a props array, a
|
|
67
|
+
* `srcFallback` key, and `href` on a favicon link. Partial derivation is worse
|
|
68
|
+
* than none: the site still breaks, but now it looks configured.
|
|
69
|
+
*
|
|
70
|
+
* Recall is what protects the project here, and precision is not what protects
|
|
71
|
+
* the visitor -- the passive/active split is. An origin admitted for images
|
|
72
|
+
* that is never referenced costs nothing; one that is missed costs a broken
|
|
73
|
+
* page.
|
|
74
|
+
*/
|
|
75
|
+
const HTTPS_URL_PATTERN = /https:\/\/[a-zA-Z0-9.-]+(?::\d{1,5})?(?![a-zA-Z0-9.:-])/g;
|
|
76
|
+
/**
|
|
77
|
+
* Reduce a URL to the `scheme://host[:port]` form CSP matches on.
|
|
78
|
+
*
|
|
79
|
+
* Returns undefined for anything unparseable, which is the same as declining
|
|
80
|
+
* to derive from it.
|
|
81
|
+
*/
|
|
82
|
+
function toCspOrigin(rawUrl) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(rawUrl);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.protocol !== "https:")
|
|
91
|
+
return undefined;
|
|
92
|
+
if (!parsed.hostname)
|
|
93
|
+
return undefined;
|
|
94
|
+
// A hostname with no dot is a bare label, not a public origin; a wildcard is
|
|
95
|
+
// an unfilled template. Neither belongs in a served policy.
|
|
96
|
+
if (!parsed.hostname.includes("."))
|
|
97
|
+
return undefined;
|
|
98
|
+
if (parsed.hostname.includes("*"))
|
|
99
|
+
return undefined;
|
|
100
|
+
return parsed.port ? `https://${parsed.hostname}:${parsed.port}` : `https://${parsed.hostname}`;
|
|
101
|
+
}
|
|
102
|
+
function scanContent(content, into) {
|
|
103
|
+
const scanned = content.length > MAX_SCANNED_CHARS_PER_FILE
|
|
104
|
+
? content.slice(0, MAX_SCANNED_CHARS_PER_FILE)
|
|
105
|
+
: content;
|
|
106
|
+
for (const match of scanned.matchAll(HTTPS_URL_PATTERN)) {
|
|
107
|
+
const origin = toCspOrigin(match[0]);
|
|
108
|
+
if (origin)
|
|
109
|
+
into.set(origin, (into.get(origin) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Collect the passive-content origins a project's source references.
|
|
114
|
+
*
|
|
115
|
+
* Pure over its input: callers hand it the file set a release pins, and the
|
|
116
|
+
* result is stable for that set, which is what lets it be computed once per
|
|
117
|
+
* release rather than per request.
|
|
118
|
+
*/
|
|
119
|
+
export function deriveCspOriginsFromSource(files) {
|
|
120
|
+
const counts = new Map();
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
if (!file?.content)
|
|
123
|
+
continue;
|
|
124
|
+
scanContent(file.content, counts);
|
|
125
|
+
}
|
|
126
|
+
if (counts.size === 0)
|
|
127
|
+
return Object.freeze({});
|
|
128
|
+
// Rank by how often an origin is referenced before applying the cap.
|
|
129
|
+
//
|
|
130
|
+
// Truncating in discovery order looked fine until it was run over a real
|
|
131
|
+
// project: one article yielded its asset CDN plus five hosts that appear
|
|
132
|
+
// only as prose links. Across a content site those one-off links vastly
|
|
133
|
+
// outnumber asset hosts, so an arbitrary cut can drop the very origin the
|
|
134
|
+
// site depends on and silently reintroduce the breakage this exists to
|
|
135
|
+
// prevent. An asset host is referenced from every page that uses it; a link
|
|
136
|
+
// in body copy is referenced once. Frequency separates them cleanly.
|
|
137
|
+
//
|
|
138
|
+
// Ties break alphabetically so the result stays deterministic for a given
|
|
139
|
+
// release, which is what lets the header and anything keyed on it be cached.
|
|
140
|
+
const ranked = [...counts.entries()]
|
|
141
|
+
.sort(([originA, countA], [originB, countB]) => countB - countA || originA.localeCompare(originB))
|
|
142
|
+
.slice(0, MAX_DERIVED_ORIGINS)
|
|
143
|
+
.map(([origin]) => origin)
|
|
144
|
+
.sort();
|
|
145
|
+
// Every retained origin is granted to all three passive directives rather
|
|
146
|
+
// than routed between them. Splitting by file extension would reintroduce a
|
|
147
|
+
// failure mode with no compensating benefit: an origin allowed for images
|
|
148
|
+
// but not media is not a boundary anyone reasons about, while guessing wrong
|
|
149
|
+
// leaves the asset blocked. The boundary that matters -- passive content
|
|
150
|
+
// versus script and connect -- is enforced by this list, not by the split.
|
|
151
|
+
const sorted = Object.freeze(ranked);
|
|
152
|
+
const derived = {};
|
|
153
|
+
for (const directive of DERIVABLE_CSP_DIRECTIVES)
|
|
154
|
+
derived[directive] = sorted;
|
|
155
|
+
return Object.freeze(derived);
|
|
156
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/security-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAYzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASjD;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,4RAgBjD,CAAC;AAMF,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExE;AAkBD,wBAAgB,aAAa,IAAI,MAAM,CAItC;
|
|
1
|
+
{"version":3,"file":"security-handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/security/http/response/security-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAYzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AASjD;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,4RAgBjD,CAAC;AAMF,wBAAgB,kCAAkC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExE;AAkBD,wBAAgB,aAAa,IAAI,MAAM,CAItC;AAkLD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,EAC9B,OAAO,CAAC,EAAE,cAAc,EACxB,iBAAiB,CAAC,EAAE,OAAO,GAC1B,MAAM,CAgBR;AA0CD,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,EAC9B,OAAO,CAAC,EAAE,cAAc,GACvB,MAAM,CAMR;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,EAC9B,OAAO,CAAC,EAAE,cAAc,EACxB,iBAAiB,CAAC,EAAE,OAAO,GAC1B,IAAI,CA8GN"}
|
|
@@ -149,7 +149,7 @@ function normalizeSources(sources) {
|
|
|
149
149
|
* directive but never the required class, which is how a project hardens past
|
|
150
150
|
* the floor without being able to break its own site.
|
|
151
151
|
*/
|
|
152
|
-
function mergeCspDirectives(required, projectCsp, nonce) {
|
|
152
|
+
function mergeCspDirectives(required, projectCsp, nonce, derived) {
|
|
153
153
|
const project = new Map();
|
|
154
154
|
for (const [key, value] of Object.entries(projectCsp ?? {})) {
|
|
155
155
|
if (value === undefined)
|
|
@@ -177,16 +177,23 @@ function mergeCspDirectives(required, projectCsp, nonce) {
|
|
|
177
177
|
const names = new Set([
|
|
178
178
|
...requiredByName.keys(),
|
|
179
179
|
...BASELINE_DIRECTIVES.keys(),
|
|
180
|
+
...Object.keys(derived ?? {}),
|
|
180
181
|
...project.keys(),
|
|
181
182
|
]);
|
|
182
183
|
const merged = Object.create(null);
|
|
183
184
|
for (const name of names) {
|
|
184
185
|
const configured = project.get(name);
|
|
185
186
|
const baseline = configured === null ? [] : BASELINE_DIRECTIVES.get(name) ?? [];
|
|
187
|
+
// Derived origins sit above the floor and below project config, and follow
|
|
188
|
+
// the baseline's `null` semantics: a project that explicitly drops a
|
|
189
|
+
// directive means it, and static analysis must not put back what the
|
|
190
|
+
// project just said it does not want.
|
|
191
|
+
const derivedForName = configured === null ? [] : derived?.[name] ?? [];
|
|
186
192
|
const additions = configured ?? [];
|
|
187
193
|
merged[name] = normalizeSources([
|
|
188
194
|
...(requiredByName.get(name) ?? []),
|
|
189
195
|
...baseline,
|
|
196
|
+
...derivedForName,
|
|
190
197
|
...additions,
|
|
191
198
|
]);
|
|
192
199
|
}
|
|
@@ -219,7 +226,7 @@ export function buildCSP(isDev, nonce, config, adapter, isVeryfrontDomain) {
|
|
|
219
226
|
// development allowance can never widen the production policy.
|
|
220
227
|
if (isDev)
|
|
221
228
|
return "";
|
|
222
|
-
return serializeDirectives(mergeCspDirectives(requiredDirectives(nonce, isVeryfrontDomain ?? false), config?.csp, nonce));
|
|
229
|
+
return serializeDirectives(mergeCspDirectives(requiredDirectives(nonce, isVeryfrontDomain ?? false), config?.csp, nonce, config?.derivedCsp));
|
|
223
230
|
}
|
|
224
231
|
/**
|
|
225
232
|
* Whether the built policy is enforced or merely reported.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/src/security/sandbox/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C,eAAO,MAAM,qBAAqB,UAAY,CAAC"}
|