@workbench-kit/electron-shell 0.0.2-prototype.0.2.31 → 0.0.2-prototype.0.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,7 @@ host-owned.
32
32
  ```ts
33
33
  import { openAllowlistedExternalLink } from '@workbench-kit/electron-shell/external-links';
34
34
  import { createApplicationQuitGuard } from '@workbench-kit/electron-shell/application-quit-guard';
35
+ import { registerPrivilegedAssetProtocolScheme } from '@workbench-kit/electron-shell/asset-protocol';
35
36
  import { requireOwnedWindowForSender } from '@workbench-kit/electron-shell/sender-security';
36
37
  import {
37
38
  createWindowControlsBridge,
@@ -43,6 +44,10 @@ import {
43
44
  resolver. `createWindowControlsBridge().toggleMaximized()` resolves to the final
44
45
  maximized state returned by the main handler.
45
46
 
47
+ `registerPrivilegedAssetProtocolScheme` owns only Electron's repeated secure
48
+ asset privileges. Call it before app readiness; the host retains its scheme,
49
+ URL parsing, cache policy, responses, and post-ready `protocol.handle` wiring.
50
+
46
51
  ## Application quit guard (`./application-quit-guard`)
47
52
 
48
53
  Electron's `before-quit` event must be vetoed synchronously, even when checking
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerPrivilegedAssetProtocolScheme = registerPrivilegedAssetProtocolScheme;
4
+ /**
5
+ * Registers an asset scheme with Electron's standard secure/fetch/stream privileges.
6
+ * Call this before app readiness; the host owns request parsing, response policy,
7
+ * and the later `protocol.handle` registration.
8
+ */
9
+ function registerPrivilegedAssetProtocolScheme(protocol, scheme, options = {}) {
10
+ protocol.registerSchemesAsPrivileged([
11
+ {
12
+ scheme,
13
+ privileges: {
14
+ corsEnabled: options.corsEnabled ?? false,
15
+ secure: true,
16
+ standard: true,
17
+ stream: true,
18
+ supportFetchAPI: true,
19
+ },
20
+ },
21
+ ]);
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-kit/electron-shell",
3
- "version": "0.0.2-prototype.0.2.31",
3
+ "version": "0.0.2-prototype.0.2.32",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "types": "./src/index.ts",
@@ -9,6 +9,9 @@
9
9
  "application-quit-guard": [
10
10
  "src/lifecycle/application-quit-guard.ts"
11
11
  ],
12
+ "asset-protocol": [
13
+ "src/assets/privileged-asset-protocol.ts"
14
+ ],
12
15
  "external-links": [
13
16
  "src/security/open-allowlisted-external-link.ts"
14
17
  ],
@@ -25,6 +28,12 @@
25
28
  },
26
29
  "exports": {
27
30
  ".": "./src/index.ts",
31
+ "./asset-protocol": {
32
+ "types": "./src/assets/privileged-asset-protocol.ts",
33
+ "require": "./dist/assets/privileged-asset-protocol.js",
34
+ "import": "./src/assets/privileged-asset-protocol.ts",
35
+ "default": "./src/assets/privileged-asset-protocol.ts"
36
+ },
28
37
  "./application-quit-guard": {
29
38
  "types": "./src/lifecycle/application-quit-guard.ts",
30
39
  "require": "./dist/lifecycle/application-quit-guard.js",
@@ -0,0 +1,43 @@
1
+ export interface PrivilegedAssetProtocolScheme {
2
+ readonly scheme: string;
3
+ readonly privileges: {
4
+ readonly corsEnabled: boolean;
5
+ readonly secure: true;
6
+ readonly standard: true;
7
+ readonly stream: true;
8
+ readonly supportFetchAPI: true;
9
+ };
10
+ }
11
+
12
+ export interface PrivilegedAssetProtocolRegistrar {
13
+ registerSchemesAsPrivileged(schemes: PrivilegedAssetProtocolScheme[]): void;
14
+ }
15
+
16
+ export interface RegisterPrivilegedAssetProtocolSchemeOptions {
17
+ /** Enable CORS only when the host's protocol response must cross origins. */
18
+ readonly corsEnabled?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Registers an asset scheme with Electron's standard secure/fetch/stream privileges.
23
+ * Call this before app readiness; the host owns request parsing, response policy,
24
+ * and the later `protocol.handle` registration.
25
+ */
26
+ export function registerPrivilegedAssetProtocolScheme(
27
+ protocol: PrivilegedAssetProtocolRegistrar,
28
+ scheme: string,
29
+ options: RegisterPrivilegedAssetProtocolSchemeOptions = {},
30
+ ): void {
31
+ protocol.registerSchemesAsPrivileged([
32
+ {
33
+ scheme,
34
+ privileges: {
35
+ corsEnabled: options.corsEnabled ?? false,
36
+ secure: true,
37
+ standard: true,
38
+ stream: true,
39
+ supportFetchAPI: true,
40
+ },
41
+ },
42
+ ]);
43
+ }
package/src/index.ts CHANGED
@@ -27,16 +27,11 @@ export {
27
27
  type IpcSenderLike,
28
28
  } from './security/require-owned-window-for-sender.js';
29
29
  export {
30
- cacheAllowlistedHttpsAsset,
31
- registerRootConfinedAssetProtocol,
32
- type AssetCachePolicy,
33
- type AssetCacheStore,
34
- type CachedAssetMeta,
35
- type FetchAllowlistedHttps,
36
- type PathRootHelpers,
37
- type PrivilegedProtocolApi,
38
- type RegisterRootConfinedAssetProtocolOptions,
39
- } from './assets/root-confined-asset-protocol.js';
30
+ registerPrivilegedAssetProtocolScheme,
31
+ type PrivilegedAssetProtocolRegistrar,
32
+ type PrivilegedAssetProtocolScheme,
33
+ type RegisterPrivilegedAssetProtocolSchemeOptions,
34
+ } from './assets/privileged-asset-protocol.js';
40
35
  export {
41
36
  createWin32WallpaperPathResolver,
42
37
  resolveWallpaperCropRect,
@@ -1,162 +0,0 @@
1
- export interface AssetCachePolicy {
2
- /** Max age in ms; expired entries are treated as missing. */
3
- readonly ttlMs: number;
4
- /** Max bytes accepted for a single cached asset. */
5
- readonly maxBytes: number;
6
- }
7
-
8
- export interface CachedAssetMeta {
9
- readonly relativePath: string;
10
- readonly contentType: string;
11
- readonly fetchedAt: number;
12
- readonly byteLength: number;
13
- }
14
-
15
- export interface AssetCacheStore {
16
- readMeta(cacheKey: string): Promise<CachedAssetMeta | null>;
17
- writeMeta(cacheKey: string, meta: CachedAssetMeta): Promise<void>;
18
- readBytes(relativePath: string): Promise<Uint8Array | null>;
19
- writeBytes(relativePath: string, bytes: Uint8Array): Promise<void>;
20
- }
21
-
22
- export interface FetchAllowlistedHttps {
23
- (url: string): Promise<{ bytes: Uint8Array; contentType: string }>;
24
- }
25
-
26
- export interface PrivilegedProtocolApi {
27
- registerSchemesAsPrivileged?: (
28
- schemes: ReadonlyArray<{ scheme: string; privileges: Record<string, boolean> }>,
29
- ) => void;
30
- handle: (
31
- scheme: string,
32
- handler: (request: { url: string }) => Promise<{ data: Uint8Array; mimeType: string }>,
33
- ) => void;
34
- }
35
-
36
- export interface PathRootHelpers {
37
- /** Resolve a relative key under the cache root; must reject escapes. */
38
- readonly resolveInsideRoot: (root: string, relativePath: string) => string;
39
- }
40
-
41
- export interface RegisterRootConfinedAssetProtocolOptions extends PathRootHelpers {
42
- readonly scheme: string;
43
- readonly cacheRoot: string;
44
- readonly protocol: PrivilegedProtocolApi;
45
- readonly cache: AssetCacheStore;
46
- readonly policy: AssetCachePolicy;
47
- /** Enable CORS for the privileged scheme only when the host explicitly requires it. */
48
- readonly corsEnabled?: boolean;
49
- readonly now?: () => number;
50
- }
51
-
52
- function relativeAssetPath(cacheKey: string): string {
53
- return `objects/${cacheKey}.bin`;
54
- }
55
-
56
- function cacheKeyFromProtocolUrl(requestUrl: string): string {
57
- const url = new URL(requestUrl);
58
- const fromHost = url.hostname.trim();
59
- if (fromHost.length > 0) {
60
- return decodeURIComponent(fromHost);
61
- }
62
- return decodeURIComponent(url.pathname.replace(/^\//, ''));
63
- }
64
-
65
- function createPrivilegedSchemePrivileges(corsEnabled: boolean): Record<string, boolean> {
66
- return {
67
- standard: true,
68
- secure: true,
69
- supportFetchAPI: true,
70
- corsEnabled,
71
- stream: true,
72
- };
73
- }
74
-
75
- /**
76
- * Populate the root-confined cache from an allowlisted HTTPS response.
77
- * Hosts own which URLs are fetched and how hash/TTL/size policy is chosen.
78
- * Inject `resolveInsideRoot` from `@workbench-kit/platform/node` (or a test fake).
79
- */
80
- export async function cacheAllowlistedHttpsAsset(
81
- options: PathRootHelpers & {
82
- readonly url: string;
83
- readonly cacheRoot: string;
84
- readonly cache: AssetCacheStore;
85
- readonly policy: AssetCachePolicy;
86
- readonly hashCacheKey: (url: string) => string;
87
- readonly fetchHttps: FetchAllowlistedHttps;
88
- readonly now?: () => number;
89
- },
90
- ): Promise<CachedAssetMeta> {
91
- const cacheKey = options.hashCacheKey(options.url);
92
- const relativePath = relativeAssetPath(cacheKey);
93
- options.resolveInsideRoot(options.cacheRoot, relativePath);
94
-
95
- const response = await options.fetchHttps(options.url);
96
- if (response.bytes.byteLength > options.policy.maxBytes) {
97
- throw new Error('Cached asset exceeds the configured maxBytes limit.');
98
- }
99
-
100
- await options.cache.writeBytes(relativePath, response.bytes);
101
- const meta: CachedAssetMeta = {
102
- relativePath,
103
- contentType: response.contentType || 'application/octet-stream',
104
- fetchedAt: (options.now ?? Date.now)(),
105
- byteLength: response.bytes.byteLength,
106
- };
107
- await options.cache.writeMeta(cacheKey, meta);
108
- return meta;
109
- }
110
-
111
- /**
112
- * Register a privileged custom protocol that serves only files under `cacheRoot`.
113
- * Unknown / expired cache keys reject; path escapes are rejected by `resolveInsideRoot`.
114
- */
115
- export function registerRootConfinedAssetProtocol(
116
- options: RegisterRootConfinedAssetProtocolOptions,
117
- ): void {
118
- const {
119
- scheme,
120
- cacheRoot,
121
- protocol,
122
- cache,
123
- policy,
124
- resolveInsideRoot,
125
- corsEnabled = false,
126
- } = options;
127
- const now = options.now ?? Date.now;
128
-
129
- protocol.registerSchemesAsPrivileged?.([
130
- {
131
- scheme,
132
- privileges: createPrivilegedSchemePrivileges(corsEnabled),
133
- },
134
- ]);
135
-
136
- protocol.handle(scheme, async (request) => {
137
- const cacheKey = cacheKeyFromProtocolUrl(request.url);
138
- if (!cacheKey) {
139
- throw new Error('Asset protocol request is missing a cache key.');
140
- }
141
-
142
- const meta = await cache.readMeta(cacheKey);
143
- if (meta === null) {
144
- throw new Error('Cached asset is not present.');
145
- }
146
- if (now() - meta.fetchedAt > policy.ttlMs) {
147
- throw new Error('Cached asset has expired.');
148
- }
149
-
150
- resolveInsideRoot(cacheRoot, meta.relativePath);
151
-
152
- const bytes = await cache.readBytes(meta.relativePath);
153
- if (bytes === null) {
154
- throw new Error('Cached asset bytes are missing.');
155
- }
156
-
157
- return {
158
- data: bytes,
159
- mimeType: meta.contentType,
160
- };
161
- });
162
- }