@harborclient/sdk 0.7.0 → 1.0.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.
Files changed (45) hide show
  1. package/dist/build/index.d.ts +59 -0
  2. package/dist/build/index.d.ts.map +1 -0
  3. package/dist/build/index.js +107 -0
  4. package/dist/components/index.d.ts +1 -0
  5. package/dist/components/index.d.ts.map +1 -1
  6. package/dist/components/index.js +1 -0
  7. package/dist/components/portalToBody.d.ts +10 -0
  8. package/dist/components/portalToBody.d.ts.map +1 -0
  9. package/dist/components/portalToBody.js +14 -0
  10. package/dist/eslint/index.d.ts +9 -0
  11. package/dist/eslint/index.d.ts.map +1 -0
  12. package/dist/eslint/index.js +18 -0
  13. package/dist/http/resolveRequest.test.js +2 -1
  14. package/dist/runtime/jsx-runtime-host.d.ts +44 -0
  15. package/dist/runtime/jsx-runtime-host.js +42 -0
  16. package/dist/runtime/react-dom.d.ts +1 -0
  17. package/dist/runtime/react-dom.js +16 -0
  18. package/dist/runtime/react.d.ts +6 -0
  19. package/dist/runtime/react.js +70 -2
  20. package/dist/runtime/reactHost.js +52 -0
  21. package/dist/runtime/store.d.ts +101 -0
  22. package/dist/runtime/store.d.ts.map +1 -1
  23. package/dist/runtime/store.js +94 -0
  24. package/dist/runtime/store.ts +203 -0
  25. package/dist/runtime/viewHost.js +2 -1
  26. package/dist/runtime-utils.d.ts +59 -0
  27. package/dist/runtime-utils.d.ts.map +1 -1
  28. package/dist/runtime-utils.js +66 -0
  29. package/dist/runtime-utils.test.js +74 -1
  30. package/dist/storage/index.d.ts +1 -0
  31. package/dist/storage/index.d.ts.map +1 -1
  32. package/dist/storage/index.js +1 -0
  33. package/dist/storage/validate.d.ts +71 -0
  34. package/dist/storage/validate.d.ts.map +1 -0
  35. package/dist/storage/validate.js +111 -0
  36. package/dist/storage/validate.test.d.ts +2 -0
  37. package/dist/storage/validate.test.d.ts.map +1 -0
  38. package/dist/storage/validate.test.js +78 -0
  39. package/dist/store.test.d.ts +2 -0
  40. package/dist/store.test.d.ts.map +1 -0
  41. package/dist/store.test.js +248 -0
  42. package/dist/types.d.ts +14 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +35 -7
  45. package/tsconfig.base.json +15 -0
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/runtime/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG;IAClD,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAChD,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;CAC7B,CAkBA;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,UAAU,CAO1F"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/runtime/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAE5C;;;;;OAKG;IACH,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAE5C;;OAEG;IACH,WAAW,IAAI,CAAC,CAAC;IAEjB;;OAEG;IACH,QAAQ,IAAI,CAAC,CAAC;IAEd;;OAEG;IACH,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB,CAAC,CAAC;IAC1C,4CAA4C;IAC5C,OAAO,EAAE,WAAW,CAAC;IAErB,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;;OAKG;IACH,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC;IAE3B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC;IAEjC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG;IAClD,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAChD,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;CAC7B,CAkBA;AAYD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CA+C5F;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,UAAU,CAO1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,EAAE,EACvD,OAAO,CAAC,EAAE,wBAAwB,GACjC,UAAU,CA0BZ"}
@@ -1,3 +1,4 @@
1
+ import { useSyncExternalStore } from './react.js';
1
2
  /**
2
3
  * Creates a module-level external store compatible with React `useSyncExternalStore`.
3
4
  *
@@ -22,6 +23,67 @@ export function createExternalStore(initial) {
22
23
  }
23
24
  };
24
25
  }
26
+ /**
27
+ * Compares two JSON-serializable values using `JSON.stringify`.
28
+ *
29
+ * @param a - First value.
30
+ * @param b - Second value.
31
+ */
32
+ function defaultEquals(a, b) {
33
+ return JSON.stringify(a) === JSON.stringify(b);
34
+ }
35
+ /**
36
+ * Creates a storage-backed external store for sharing state across plugin webviews.
37
+ *
38
+ * Separate plugin webviews do not share memory; use {@link syncOnWindowFocus} to
39
+ * reload when another surface writes storage.
40
+ *
41
+ * @param options - Storage key, parse/equals helpers, and the plugin storage API.
42
+ */
43
+ export function createStorageStore(options) {
44
+ const { storage, key, parse, equals = defaultEquals, keepCurrentWhenMissing = false } = options;
45
+ const external = createExternalStore(parse(undefined));
46
+ /**
47
+ * Reloads from storage and updates subscribers when the parsed snapshot changed.
48
+ */
49
+ async function reloadFromStorage() {
50
+ const raw = await storage.get(key);
51
+ if (raw === undefined && keepCurrentWhenMissing) {
52
+ return;
53
+ }
54
+ const next = parse(raw);
55
+ const current = external.getSnapshot();
56
+ if (!equals(current, next)) {
57
+ external.setState(next);
58
+ }
59
+ }
60
+ /**
61
+ * Updates the snapshot, notifies subscribers, and persists when the value changed.
62
+ *
63
+ * @param next - New snapshot value.
64
+ */
65
+ async function set(next) {
66
+ const current = external.getSnapshot();
67
+ if (equals(current, next)) {
68
+ return;
69
+ }
70
+ external.setState(next);
71
+ await storage.set(key, next);
72
+ }
73
+ /**
74
+ * React hook returning the current storage-backed snapshot.
75
+ */
76
+ function useValue() {
77
+ return useSyncExternalStore(external.subscribe, external.getSnapshot, external.getSnapshot);
78
+ }
79
+ return {
80
+ subscribe: external.subscribe,
81
+ getSnapshot: external.getSnapshot,
82
+ useValue,
83
+ reloadFromStorage,
84
+ set
85
+ };
86
+ }
25
87
  /**
26
88
  * Starts an interval and returns a disposable that clears it on deactivation.
27
89
  *
@@ -36,3 +98,35 @@ export function setIntervalDisposable(callback, intervalMs) {
36
98
  }
37
99
  };
38
100
  }
101
+ /**
102
+ * Reloads one or more storage-backed stores when the window regains focus or
103
+ * becomes visible, with optional polling for live cross-webview updates.
104
+ *
105
+ * Push the returned disposable onto `hc.subscriptions`, or dispose it from a
106
+ * React effect cleanup.
107
+ *
108
+ * @param stores - One store or an array of stores to reload together.
109
+ * @param options - Optional polling interval in milliseconds.
110
+ */
111
+ export function syncOnWindowFocus(stores, options) {
112
+ const list = Array.isArray(stores) ? stores : [stores];
113
+ /**
114
+ * Reloads every registered store from plugin storage.
115
+ */
116
+ const reload = () => {
117
+ for (const store of list) {
118
+ void store.reloadFromStorage();
119
+ }
120
+ };
121
+ window.addEventListener('focus', reload);
122
+ document.addEventListener('visibilitychange', reload);
123
+ reload();
124
+ const intervalDisposable = options?.intervalMs !== undefined ? setIntervalDisposable(reload, options.intervalMs) : null;
125
+ return {
126
+ dispose: () => {
127
+ window.removeEventListener('focus', reload);
128
+ document.removeEventListener('visibilitychange', reload);
129
+ intervalDisposable?.dispose();
130
+ }
131
+ };
132
+ }
@@ -1,4 +1,99 @@
1
1
  import type { Disposable } from '../types.js';
2
+ import { useSyncExternalStore } from './react.js';
3
+
4
+ /**
5
+ * Minimal storage surface used by {@link createStorageStore}.
6
+ */
7
+ export interface StorageLike {
8
+ /**
9
+ * Returns the stored value for a key.
10
+ *
11
+ * @param key - Storage key within the plugin namespace.
12
+ */
13
+ get<T>(key: string): Promise<T | undefined>;
14
+
15
+ /**
16
+ * Persists a JSON-serializable value.
17
+ *
18
+ * @param key - Storage key within the plugin namespace.
19
+ * @param value - Value to persist.
20
+ */
21
+ set<T>(key: string, value: T): Promise<void>;
22
+ }
23
+
24
+ /**
25
+ * Reactive store backed by plugin storage and compatible with React
26
+ * `useSyncExternalStore`.
27
+ */
28
+ export interface StorageStore<T> {
29
+ /**
30
+ * Subscribes to snapshot changes for `useSyncExternalStore`.
31
+ *
32
+ * @param listener - Callback invoked when the snapshot changes.
33
+ */
34
+ subscribe(listener: () => void): () => void;
35
+
36
+ /**
37
+ * Returns the current in-memory snapshot.
38
+ */
39
+ getSnapshot(): T;
40
+
41
+ /**
42
+ * React hook returning the current snapshot.
43
+ */
44
+ useValue(): T;
45
+
46
+ /**
47
+ * Reloads from storage and notifies subscribers when the parsed value changed.
48
+ */
49
+ reloadFromStorage(): Promise<void>;
50
+
51
+ /**
52
+ * Updates the in-memory snapshot, notifies subscribers, and persists to storage.
53
+ *
54
+ * @param next - New snapshot value.
55
+ */
56
+ set(next: T): Promise<void>;
57
+ }
58
+
59
+ /**
60
+ * Options for {@link createStorageStore}.
61
+ */
62
+ export interface CreateStorageStoreOptions<T> {
63
+ /** Plugin storage API from `hc.storage`. */
64
+ storage: StorageLike;
65
+
66
+ /** Storage key within the plugin namespace. */
67
+ key: string;
68
+
69
+ /**
70
+ * Validates and hydrates a raw storage value into a typed snapshot.
71
+ * Called with `undefined` when the key has never been set.
72
+ *
73
+ * @param raw - Raw value from storage, or `undefined` when absent.
74
+ */
75
+ parse: (raw: unknown) => T;
76
+
77
+ /**
78
+ * Compares two snapshots to skip no-op reloads and writes.
79
+ * Defaults to `JSON.stringify` equality.
80
+ */
81
+ equals?: (a: T, b: T) => boolean;
82
+
83
+ /**
84
+ * When true, {@link StorageStore.reloadFromStorage} leaves the snapshot
85
+ * unchanged if the storage key is absent. Default false (apply `parse(undefined)`).
86
+ */
87
+ keepCurrentWhenMissing?: boolean;
88
+ }
89
+
90
+ /**
91
+ * Options for {@link syncOnWindowFocus}.
92
+ */
93
+ export interface SyncOnWindowFocusOptions {
94
+ /** Optional polling interval in milliseconds in addition to focus/visibility reloads. */
95
+ intervalMs?: number;
96
+ }
2
97
 
3
98
  /**
4
99
  * Creates a module-level external store compatible with React `useSyncExternalStore`.
@@ -29,6 +124,73 @@ export function createExternalStore<T>(initial: T): {
29
124
  };
30
125
  }
31
126
 
127
+ /**
128
+ * Compares two JSON-serializable values using `JSON.stringify`.
129
+ *
130
+ * @param a - First value.
131
+ * @param b - Second value.
132
+ */
133
+ function defaultEquals<T>(a: T, b: T): boolean {
134
+ return JSON.stringify(a) === JSON.stringify(b);
135
+ }
136
+
137
+ /**
138
+ * Creates a storage-backed external store for sharing state across plugin webviews.
139
+ *
140
+ * Separate plugin webviews do not share memory; use {@link syncOnWindowFocus} to
141
+ * reload when another surface writes storage.
142
+ *
143
+ * @param options - Storage key, parse/equals helpers, and the plugin storage API.
144
+ */
145
+ export function createStorageStore<T>(options: CreateStorageStoreOptions<T>): StorageStore<T> {
146
+ const { storage, key, parse, equals = defaultEquals, keepCurrentWhenMissing = false } = options;
147
+ const external = createExternalStore(parse(undefined));
148
+
149
+ /**
150
+ * Reloads from storage and updates subscribers when the parsed snapshot changed.
151
+ */
152
+ async function reloadFromStorage(): Promise<void> {
153
+ const raw = await storage.get(key);
154
+ if (raw === undefined && keepCurrentWhenMissing) {
155
+ return;
156
+ }
157
+ const next = parse(raw);
158
+ const current = external.getSnapshot();
159
+ if (!equals(current, next)) {
160
+ external.setState(next);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Updates the snapshot, notifies subscribers, and persists when the value changed.
166
+ *
167
+ * @param next - New snapshot value.
168
+ */
169
+ async function set(next: T): Promise<void> {
170
+ const current = external.getSnapshot();
171
+ if (equals(current, next)) {
172
+ return;
173
+ }
174
+ external.setState(next);
175
+ await storage.set(key, next);
176
+ }
177
+
178
+ /**
179
+ * React hook returning the current storage-backed snapshot.
180
+ */
181
+ function useValue(): T {
182
+ return useSyncExternalStore(external.subscribe, external.getSnapshot, external.getSnapshot);
183
+ }
184
+
185
+ return {
186
+ subscribe: external.subscribe,
187
+ getSnapshot: external.getSnapshot,
188
+ useValue,
189
+ reloadFromStorage,
190
+ set
191
+ };
192
+ }
193
+
32
194
  /**
33
195
  * Starts an interval and returns a disposable that clears it on deactivation.
34
196
  *
@@ -43,3 +205,44 @@ export function setIntervalDisposable(callback: () => void, intervalMs: number):
43
205
  }
44
206
  };
45
207
  }
208
+
209
+ /**
210
+ * Reloads one or more storage-backed stores when the window regains focus or
211
+ * becomes visible, with optional polling for live cross-webview updates.
212
+ *
213
+ * Push the returned disposable onto `hc.subscriptions`, or dispose it from a
214
+ * React effect cleanup.
215
+ *
216
+ * @param stores - One store or an array of stores to reload together.
217
+ * @param options - Optional polling interval in milliseconds.
218
+ */
219
+ export function syncOnWindowFocus(
220
+ stores: StorageStore<unknown> | StorageStore<unknown>[],
221
+ options?: SyncOnWindowFocusOptions
222
+ ): Disposable {
223
+ const list = Array.isArray(stores) ? stores : [stores];
224
+
225
+ /**
226
+ * Reloads every registered store from plugin storage.
227
+ */
228
+ const reload = (): void => {
229
+ for (const store of list) {
230
+ void store.reloadFromStorage();
231
+ }
232
+ };
233
+
234
+ window.addEventListener('focus', reload);
235
+ document.addEventListener('visibilitychange', reload);
236
+ reload();
237
+
238
+ const intervalDisposable =
239
+ options?.intervalMs !== undefined ? setIntervalDisposable(reload, options.intervalMs) : null;
240
+
241
+ return {
242
+ dispose: () => {
243
+ window.removeEventListener('focus', reload);
244
+ document.removeEventListener('visibilitychange', reload);
245
+ intervalDisposable?.dispose();
246
+ }
247
+ };
248
+ }
@@ -7,7 +7,7 @@ import {
7
7
  parseViewHostRole,
8
8
  resolveContributionKindFromUrl
9
9
  } from './createBridgedPluginContext.js';
10
- import { setHostReact } from './reactHost.js';
10
+ import { setHostReact, setHostReactDom } from './reactHost.js';
11
11
 
12
12
  /**
13
13
  * Bootstraps an isolated plugin webview shell.
@@ -47,6 +47,7 @@ export async function bootstrapViewHost(options = {}) {
47
47
  ]);
48
48
 
49
49
  setHostReact(React);
50
+ setHostReactDom(ReactDOM);
50
51
 
51
52
  const manifest = await fetch(`harbor-plugin://${pluginId}/manifest.json`).then((response) =>
52
53
  response.json()
@@ -33,4 +33,63 @@ export declare function truncateBody(body: string, maxBytes: number, suffix?: st
33
33
  body: string;
34
34
  truncated: boolean;
35
35
  };
36
+ /**
37
+ * Minimum log level emitted by {@link createLogger}.
38
+ *
39
+ * Messages below the active level are suppressed. `silent` suppresses all output.
40
+ */
41
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
42
+ /**
43
+ * Plugin-scoped logger with consistent `[pluginId]` prefixing and level control.
44
+ *
45
+ * Safe in the SES plugin main runtime where only `console`, `Date`, and `Math`
46
+ * globals are available.
47
+ */
48
+ export interface Logger {
49
+ /**
50
+ * Logs a debug message when the active level is `debug`.
51
+ *
52
+ * @param args - Values forwarded to the console after the prefix.
53
+ */
54
+ debug(...args: unknown[]): void;
55
+ /**
56
+ * Logs an informational message when the active level is `debug` or `info`.
57
+ *
58
+ * @param args - Values forwarded to the console after the prefix.
59
+ */
60
+ info(...args: unknown[]): void;
61
+ /**
62
+ * Logs a warning when the active level is below `error`.
63
+ *
64
+ * @param args - Values forwarded to the console after the prefix.
65
+ */
66
+ warn(...args: unknown[]): void;
67
+ /**
68
+ * Logs an error when the active level is not `silent`.
69
+ *
70
+ * @param args - Values forwarded to the console after the prefix.
71
+ */
72
+ error(...args: unknown[]): void;
73
+ /**
74
+ * Changes the minimum log level for subsequent calls.
75
+ *
76
+ * @param level - New minimum level.
77
+ */
78
+ setLevel(level: LogLevel): void;
79
+ }
80
+ /**
81
+ * Creates a plugin-scoped logger that prefixes every message with `[pluginId]`.
82
+ *
83
+ * Routes through `console` only so the logger works in both the renderer and
84
+ * the SES-hardened main runtime. When `console.warn` or `console.error` are
85
+ * unavailable, falls back to `console.log`.
86
+ *
87
+ * @param pluginId - Manifest id used as the log prefix.
88
+ * @param options - Optional initial configuration.
89
+ * @param options.level - Minimum level to emit. Defaults to `info`.
90
+ * @returns Logger with level-filtered `debug`, `info`, `warn`, and `error` methods.
91
+ */
92
+ export declare function createLogger(pluginId: string, options?: {
93
+ level?: LogLevel;
94
+ }): Logger;
36
95
  //# sourceMappingURL=runtime-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-utils.d.ts","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,SAAO,GAAG,MAAM,CAK9C;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAmBhD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+BvE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,SAAsD,GAC3D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAQtC"}
1
+ {"version":3,"file":"runtime-utils.d.ts","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,SAAO,GAAG,MAAM,CAK9C;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAmBhD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+BvE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,SAAsD,GAC3D;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAQtC;AAED;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB;;;;OAIG;IACH,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAEhC;;;;OAIG;IACH,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAE/B;;;;OAIG;IACH,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAE/B;;;;OAIG;IACH,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAEhC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;CACjC;AAUD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,QAAQ,CAAA;CAAE,GAAG,MAAM,CAuDrF"}
@@ -99,3 +99,69 @@ export function truncateBody(body, maxBytes, suffix = `\n\n[truncated — body e
99
99
  truncated: true
100
100
  };
101
101
  }
102
+ const LOG_LEVEL_RANK = {
103
+ debug: 0,
104
+ info: 1,
105
+ warn: 2,
106
+ error: 3,
107
+ silent: 4
108
+ };
109
+ /**
110
+ * Creates a plugin-scoped logger that prefixes every message with `[pluginId]`.
111
+ *
112
+ * Routes through `console` only so the logger works in both the renderer and
113
+ * the SES-hardened main runtime. When `console.warn` or `console.error` are
114
+ * unavailable, falls back to `console.log`.
115
+ *
116
+ * @param pluginId - Manifest id used as the log prefix.
117
+ * @param options - Optional initial configuration.
118
+ * @param options.level - Minimum level to emit. Defaults to `info`.
119
+ * @returns Logger with level-filtered `debug`, `info`, `warn`, and `error` methods.
120
+ */
121
+ export function createLogger(pluginId, options) {
122
+ const prefix = `[${pluginId}]`;
123
+ let level = options?.level ?? 'info';
124
+ /**
125
+ * Emits a message when its severity meets the active level threshold.
126
+ *
127
+ * @param messageLevel - Severity of the message being logged.
128
+ * @param write - Console method to invoke when the message is not suppressed.
129
+ * @param args - Values forwarded after the prefix.
130
+ */
131
+ function log(messageLevel, write, ...args) {
132
+ if (LOG_LEVEL_RANK[messageLevel] < LOG_LEVEL_RANK[level]) {
133
+ return;
134
+ }
135
+ write(prefix, ...args);
136
+ }
137
+ const consoleLog = (...values) => {
138
+ console.log(...values);
139
+ };
140
+ return {
141
+ debug(...args) {
142
+ log('debug', consoleLog, ...args);
143
+ },
144
+ info(...args) {
145
+ log('info', consoleLog, ...args);
146
+ },
147
+ warn(...args) {
148
+ const write = typeof console.warn === 'function'
149
+ ? (...values) => {
150
+ console.warn(...values);
151
+ }
152
+ : consoleLog;
153
+ log('warn', write, ...args);
154
+ },
155
+ error(...args) {
156
+ const write = typeof console.error === 'function'
157
+ ? (...values) => {
158
+ console.error(...values);
159
+ }
160
+ : consoleLog;
161
+ log('error', write, ...args);
162
+ },
163
+ setLevel(nextLevel) {
164
+ level = nextLevel;
165
+ }
166
+ };
167
+ }
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it, jest } from '@jest/globals';
2
- import { byteLength, randomId, truncateBody, truncateToBytes } from './runtime-utils.js';
2
+ import { byteLength, createLogger, randomId, truncateBody, truncateToBytes } from './runtime-utils.js';
3
3
  const originalTextEncoder = globalThis.TextEncoder;
4
4
  const originalCrypto = globalThis.crypto;
5
5
  afterEach(() => {
@@ -102,3 +102,76 @@ describe('truncateBody', () => {
102
102
  });
103
103
  });
104
104
  });
105
+ describe('createLogger', () => {
106
+ it('prefixes messages with [pluginId]', () => {
107
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
108
+ const logger = createLogger('my-plugin');
109
+ logger.info('hello');
110
+ expect(logSpy).toHaveBeenCalledWith('[my-plugin]', 'hello');
111
+ logSpy.mockRestore();
112
+ });
113
+ it('filters messages below the active level', () => {
114
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
115
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
116
+ const logger = createLogger('my-plugin', { level: 'warn' });
117
+ logger.debug('hidden');
118
+ logger.info('hidden');
119
+ logger.warn('visible');
120
+ expect(logSpy).not.toHaveBeenCalled();
121
+ expect(warnSpy).toHaveBeenCalledTimes(1);
122
+ expect(warnSpy).toHaveBeenCalledWith('[my-plugin]', 'visible');
123
+ logSpy.mockRestore();
124
+ warnSpy.mockRestore();
125
+ });
126
+ it('updates the active level via setLevel', () => {
127
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
128
+ const logger = createLogger('my-plugin');
129
+ logger.info('before');
130
+ logger.setLevel('silent');
131
+ logger.info('after');
132
+ expect(logSpy).toHaveBeenCalledTimes(1);
133
+ expect(logSpy).toHaveBeenCalledWith('[my-plugin]', 'before');
134
+ logSpy.mockRestore();
135
+ });
136
+ it('suppresses all output at silent level', () => {
137
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
138
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
139
+ const logger = createLogger('my-plugin', { level: 'silent' });
140
+ logger.debug('a');
141
+ logger.info('b');
142
+ logger.warn('c');
143
+ logger.error('d');
144
+ expect(logSpy).not.toHaveBeenCalled();
145
+ expect(errorSpy).not.toHaveBeenCalled();
146
+ logSpy.mockRestore();
147
+ errorSpy.mockRestore();
148
+ });
149
+ it('routes warn and error through console.warn and console.error when available', () => {
150
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
151
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
152
+ const logger = createLogger('my-plugin', { level: 'debug' });
153
+ logger.warn('warned');
154
+ logger.error('failed');
155
+ expect(warnSpy).toHaveBeenCalledWith('[my-plugin]', 'warned');
156
+ expect(errorSpy).toHaveBeenCalledWith('[my-plugin]', 'failed');
157
+ warnSpy.mockRestore();
158
+ errorSpy.mockRestore();
159
+ });
160
+ it('falls back to console.log when console.warn and console.error are unavailable', () => {
161
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
162
+ const originalWarn = console.warn;
163
+ const originalError = console.error;
164
+ // @ts-expect-error — exercise SES main-runtime fallback
165
+ console.warn = undefined;
166
+ // @ts-expect-error — exercise SES main-runtime fallback
167
+ console.error = undefined;
168
+ const logger = createLogger('my-plugin', { level: 'debug' });
169
+ logger.warn('warned');
170
+ logger.error('failed');
171
+ expect(logSpy).toHaveBeenCalledWith('[my-plugin]', 'warned');
172
+ expect(logSpy).toHaveBeenCalledWith('[my-plugin]', 'failed');
173
+ console.warn = originalWarn;
174
+ console.error = originalError;
175
+ logSpy.mockRestore();
176
+ });
177
+ });
@@ -1,2 +1,3 @@
1
1
  export { createCappedList, mergeById, type CreateCappedListOptions, type MergeByIdOptions } from './cappedList.js';
2
+ export { arrayOf, asRecord, bool, isRecord, num, numArray, oneOf, recordOf, str, strArray } from './validate.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,OAAO,EACP,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,GAAG,EACH,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,GAAG,EACH,QAAQ,EACT,MAAM,eAAe,CAAC"}
@@ -1 +1,2 @@
1
1
  export { createCappedList, mergeById } from './cappedList.js';
2
+ export { arrayOf, asRecord, bool, isRecord, num, numArray, oneOf, recordOf, str, strArray } from './validate.js';
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Returns true when a value is a plain object record (not null, not an array).
3
+ *
4
+ * @param value - Candidate value from plugin storage.
5
+ */
6
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
7
+ /**
8
+ * Narrows an unknown storage value to a string-keyed record, or null when invalid.
9
+ *
10
+ * @param value - Raw value from plugin storage.
11
+ */
12
+ export declare function asRecord(value: unknown): Record<string, unknown> | null;
13
+ /**
14
+ * Returns a string when the value is a string; otherwise the fallback.
15
+ *
16
+ * @param value - Candidate field value.
17
+ * @param fallback - Value used when the candidate is not a string.
18
+ */
19
+ export declare function str<F>(value: unknown, fallback: F): string | F;
20
+ /**
21
+ * Returns a finite number when the value is a number; otherwise the fallback.
22
+ *
23
+ * @param value - Candidate field value.
24
+ * @param fallback - Value used when the candidate is not a finite number.
25
+ */
26
+ export declare function num<F>(value: unknown, fallback: F): number | F;
27
+ /**
28
+ * Returns a boolean when the value is a boolean; otherwise the fallback.
29
+ *
30
+ * @param value - Candidate field value.
31
+ * @param fallback - Value used when the candidate is not a boolean.
32
+ */
33
+ export declare function bool<F>(value: unknown, fallback: F): boolean | F;
34
+ /**
35
+ * Returns the candidate when it is one of the allowed string literals; otherwise the fallback.
36
+ *
37
+ * @param value - Candidate field value.
38
+ * @param allowed - Permitted string literals.
39
+ * @param fallback - Value used when the candidate is not in {@link allowed}.
40
+ */
41
+ export declare function oneOf<T extends string, F>(value: unknown, allowed: readonly T[], fallback: F): T | F;
42
+ /**
43
+ * Filters an array to finite numbers; returns an empty array when the input is not an array.
44
+ *
45
+ * @param value - Candidate array from plugin storage.
46
+ */
47
+ export declare function numArray(value: unknown): number[];
48
+ /**
49
+ * Filters an array to non-empty strings; returns an empty array when the input is not an array.
50
+ *
51
+ * @param value - Candidate array from plugin storage.
52
+ */
53
+ export declare function strArray(value: unknown): string[];
54
+ /**
55
+ * Filters an array through a type guard; returns an empty array when the input is not an array.
56
+ *
57
+ * @param value - Candidate array from plugin storage.
58
+ * @param guard - Predicate that narrows each element.
59
+ */
60
+ export declare function arrayOf<T>(value: unknown, guard: (entry: unknown) => entry is T): T[];
61
+ /**
62
+ * Builds a string-keyed record from entries that pass a type guard.
63
+ *
64
+ * Non-record inputs (including arrays) return an empty object so callers can treat
65
+ * malformed storage as "no data" without throwing.
66
+ *
67
+ * @param value - Raw value from plugin storage.
68
+ * @param guard - Predicate that narrows each property value.
69
+ */
70
+ export declare function recordOf<T>(value: unknown, guard: (entry: unknown) => entry is T): Record<string, T>;
71
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/storage/validate.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEvE;AAED;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,GAAG,CAAC,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,GAAG,CAAC,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,GAAG,CAAC,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACvC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,QAAQ,EAAE,CAAC,GACV,CAAC,GAAG,CAAC,CAIP;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAOjD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAKjD;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAKrF;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,KAAK,IAAI,CAAC,GACpC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAYnB"}