@workflow/web-shared 4.1.1 → 4.1.2

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 (41) hide show
  1. package/dist/components/event-list-view.d.ts +3 -1
  2. package/dist/components/event-list-view.d.ts.map +1 -1
  3. package/dist/components/event-list-view.js +53 -53
  4. package/dist/components/event-list-view.js.map +1 -1
  5. package/dist/components/index.d.ts +1 -1
  6. package/dist/components/index.d.ts.map +1 -1
  7. package/dist/components/index.js +1 -1
  8. package/dist/components/index.js.map +1 -1
  9. package/dist/components/sidebar/attribute-panel.d.ts +5 -1
  10. package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
  11. package/dist/components/sidebar/attribute-panel.js +37 -25
  12. package/dist/components/sidebar/attribute-panel.js.map +1 -1
  13. package/dist/components/sidebar/entity-detail-panel.d.ts +3 -1
  14. package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
  15. package/dist/components/sidebar/entity-detail-panel.js +4 -3
  16. package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
  17. package/dist/components/ui/data-inspector.d.ts +14 -1
  18. package/dist/components/ui/data-inspector.d.ts.map +1 -1
  19. package/dist/components/ui/data-inspector.js +36 -11
  20. package/dist/components/ui/data-inspector.js.map +1 -1
  21. package/dist/components/workflow-trace-view.d.ts +3 -1
  22. package/dist/components/workflow-trace-view.d.ts.map +1 -1
  23. package/dist/components/workflow-trace-view.js +2 -2
  24. package/dist/components/workflow-trace-view.js.map +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/lib/hydration.d.ts +7 -0
  30. package/dist/lib/hydration.d.ts.map +1 -1
  31. package/dist/lib/hydration.js +25 -0
  32. package/dist/lib/hydration.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/components/event-list-view.tsx +181 -173
  35. package/src/components/index.ts +6 -1
  36. package/src/components/sidebar/attribute-panel.tsx +150 -106
  37. package/src/components/sidebar/entity-detail-panel.tsx +13 -7
  38. package/src/components/ui/data-inspector.tsx +91 -19
  39. package/src/components/workflow-trace-view.tsx +4 -0
  40. package/src/index.ts +1 -0
  41. package/src/lib/hydration.ts +26 -0
@@ -5,11 +5,11 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
5
5
  import type { ModelMessage } from 'ai';
6
6
  import { Lock } from 'lucide-react';
7
7
  import type { KeyboardEvent, ReactNode } from 'react';
8
- import { useCallback, useMemo, useState } from 'react';
8
+ import { useCallback, useContext, useMemo, useState } from 'react';
9
9
  import { isEncryptedMarker, isExpiredMarker } from '../../lib/hydration';
10
10
  import { useToast } from '../../lib/toast';
11
11
  import { extractConversation, isDoStreamStep } from '../../lib/utils';
12
- import { StreamClickContext } from '../ui/data-inspector';
12
+ import { DecryptClickContext, StreamClickContext } from '../ui/data-inspector';
13
13
  import { ErrorCard } from '../ui/error-card';
14
14
  import {
15
15
  ErrorStackBlock,
@@ -180,9 +180,42 @@ function ConversationWithTabs({
180
180
  * with the lucide Lock icon matching the title bar Decrypt button.
181
181
  */
182
182
  function EncryptedFieldBlock() {
183
+ const ctx = useContext(DecryptClickContext);
184
+ if (ctx) {
185
+ return (
186
+ <button
187
+ type="button"
188
+ onClick={ctx.onDecrypt}
189
+ disabled={ctx.isDecrypting}
190
+ className="flex w-full items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-xs cursor-pointer transition-colors"
191
+ style={{
192
+ borderColor: 'var(--ds-gray-400)',
193
+ backgroundColor: 'var(--ds-gray-100)',
194
+ color: 'var(--ds-gray-700)',
195
+ opacity: ctx.isDecrypting ? 0.6 : 1,
196
+ }}
197
+ title="Click to decrypt"
198
+ >
199
+ {ctx.isDecrypting ? (
200
+ <span
201
+ className="h-3 w-3 animate-spin rounded-full border-2"
202
+ style={{
203
+ borderColor: 'var(--ds-gray-400)',
204
+ borderTopColor: 'var(--ds-gray-700)',
205
+ }}
206
+ />
207
+ ) : (
208
+ <Lock className="h-3 w-3" />
209
+ )}
210
+ <span className="font-medium">
211
+ {ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}
212
+ </span>
213
+ </button>
214
+ );
215
+ }
183
216
  return (
184
217
  <div
185
- className="flex items-center gap-1.5 rounded-md border px-3 py-2 text-xs"
218
+ className="flex w-full items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-xs"
186
219
  style={{
187
220
  borderColor: 'var(--ds-gray-300)',
188
221
  backgroundColor: 'var(--ds-gray-100)',
@@ -689,6 +722,8 @@ export const AttributePanel = ({
689
722
  error,
690
723
  expiredAt,
691
724
  onStreamClick,
725
+ onDecrypt,
726
+ isDecrypting = false,
692
727
  resource,
693
728
  }: {
694
729
  data: Record<string, unknown>;
@@ -698,6 +733,10 @@ export const AttributePanel = ({
698
733
  expiredAt?: string | Date;
699
734
  /** Callback when a stream reference is clicked */
700
735
  onStreamClick?: (streamId: string) => void;
736
+ /** Callback when an encrypted marker is clicked (triggers decryption) */
737
+ onDecrypt?: () => void;
738
+ /** Whether decryption is currently in progress */
739
+ isDecrypting?: boolean;
701
740
  /** Resource type of the selected span — used to show targeted loading skeletons. */
702
741
  resource?: string;
703
742
  }) => {
@@ -798,116 +837,121 @@ export const AttributePanel = ({
798
837
 
799
838
  return (
800
839
  <StreamClickContext.Provider value={onStreamClick}>
801
- <div>
802
- {/* Basic attributes in a vertical layout with border */}
803
- {visibleBasicAttributes.length > 0 && (
804
- <div
805
- className="mb-3 flex flex-col overflow-hidden rounded-lg border"
806
- style={{
807
- borderColor: 'var(--ds-gray-300)',
808
- }}
809
- >
810
- {orderedBasicAttributes.map((attribute, index) => {
811
- const displayValue = attributeToDisplayFn[
812
- attribute as keyof typeof attributeToDisplayFn
813
- ]?.(displayData[attribute as keyof typeof displayData]);
814
- const isModuleSpecifier = attribute === 'moduleSpecifier';
815
- const moduleSpecifierValue =
816
- typeof displayValue === 'string'
817
- ? displayValue
818
- : String(displayValue ?? displayData.moduleSpecifier ?? '');
819
- const shouldCapitalizeLabel = attribute !== 'workflowCoreVersion';
820
- const showResumeAtSkeleton =
821
- isLoading && resource === 'sleep' && !displayData.resumeAt;
822
- const showDivider =
823
- index < orderedBasicAttributes.length - 1 ||
824
- showResumeAtSkeleton;
825
-
826
- return (
827
- <div key={attribute} className="py-1">
840
+ <DecryptClickContext.Provider
841
+ value={onDecrypt ? { onDecrypt, isDecrypting } : undefined}
842
+ >
843
+ <div>
844
+ {/* Basic attributes in a vertical layout with border */}
845
+ {visibleBasicAttributes.length > 0 && (
846
+ <div
847
+ className="mb-3 flex flex-col overflow-hidden rounded-lg border"
848
+ style={{
849
+ borderColor: 'var(--ds-gray-300)',
850
+ }}
851
+ >
852
+ {orderedBasicAttributes.map((attribute, index) => {
853
+ const displayValue = attributeToDisplayFn[
854
+ attribute as keyof typeof attributeToDisplayFn
855
+ ]?.(displayData[attribute as keyof typeof displayData]);
856
+ const isModuleSpecifier = attribute === 'moduleSpecifier';
857
+ const moduleSpecifierValue =
858
+ typeof displayValue === 'string'
859
+ ? displayValue
860
+ : String(displayValue ?? displayData.moduleSpecifier ?? '');
861
+ const shouldCapitalizeLabel =
862
+ attribute !== 'workflowCoreVersion';
863
+ const showResumeAtSkeleton =
864
+ isLoading && resource === 'sleep' && !displayData.resumeAt;
865
+ const showDivider =
866
+ index < orderedBasicAttributes.length - 1 ||
867
+ showResumeAtSkeleton;
868
+
869
+ return (
870
+ <div key={attribute} className="py-1">
871
+ <div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
872
+ <span
873
+ className={
874
+ shouldCapitalizeLabel
875
+ ? 'text-[14px] first-letter:uppercase'
876
+ : 'text-[14px]'
877
+ }
878
+ style={{ color: 'var(--ds-gray-700)' }}
879
+ >
880
+ {getAttributeDisplayName(attribute)}
881
+ </span>
882
+ {isModuleSpecifier ? (
883
+ <button
884
+ type="button"
885
+ className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
886
+ style={{
887
+ color: 'var(--ds-gray-1000)',
888
+ background: 'transparent',
889
+ border: 'none',
890
+ padding: 0,
891
+ }}
892
+ title={moduleSpecifierValue}
893
+ onClick={() =>
894
+ handleCopyModuleSpecifier(moduleSpecifierValue)
895
+ }
896
+ >
897
+ {moduleSpecifierValue}
898
+ </button>
899
+ ) : (
900
+ <span
901
+ className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
902
+ style={{ color: 'var(--ds-gray-1000)' }}
903
+ >
904
+ {displayValue}
905
+ </span>
906
+ )}
907
+ </div>
908
+ {showDivider ? (
909
+ <div
910
+ className="mx-2.5 border-b"
911
+ style={{ borderColor: 'var(--ds-gray-300)' }}
912
+ />
913
+ ) : null}
914
+ </div>
915
+ );
916
+ })}
917
+ {isLoading && resource === 'sleep' && !displayData.resumeAt && (
918
+ <div className="py-1">
828
919
  <div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
829
920
  <span
830
- className={
831
- shouldCapitalizeLabel
832
- ? 'text-[14px] first-letter:uppercase'
833
- : 'text-[14px]'
834
- }
921
+ className="text-[14px] first-letter:uppercase"
835
922
  style={{ color: 'var(--ds-gray-700)' }}
836
923
  >
837
- {getAttributeDisplayName(attribute)}
924
+ resumeAt
838
925
  </span>
839
- {isModuleSpecifier ? (
840
- <button
841
- type="button"
842
- className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
843
- style={{
844
- color: 'var(--ds-gray-1000)',
845
- background: 'transparent',
846
- border: 'none',
847
- padding: 0,
848
- }}
849
- title={moduleSpecifierValue}
850
- onClick={() =>
851
- handleCopyModuleSpecifier(moduleSpecifierValue)
852
- }
853
- >
854
- {moduleSpecifierValue}
855
- </button>
856
- ) : (
857
- <span
858
- className="min-w-0 max-w-[70%] truncate text-right text-[13px] font-mono"
859
- style={{ color: 'var(--ds-gray-1000)' }}
860
- >
861
- {displayValue}
862
- </span>
863
- )}
926
+ <Skeleton className="h-4 w-[55%]" />
864
927
  </div>
865
- {showDivider ? (
866
- <div
867
- className="mx-2.5 border-b"
868
- style={{ borderColor: 'var(--ds-gray-300)' }}
869
- />
870
- ) : null}
871
- </div>
872
- );
873
- })}
874
- {isLoading && resource === 'sleep' && !displayData.resumeAt && (
875
- <div className="py-1">
876
- <div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
877
- <span
878
- className="text-[14px] first-letter:uppercase"
879
- style={{ color: 'var(--ds-gray-700)' }}
880
- >
881
- resumeAt
882
- </span>
883
- <Skeleton className="h-4 w-[55%]" />
884
928
  </div>
885
- </div>
886
- )}
887
- </div>
888
- )}
889
- {error ? (
890
- <ErrorCard
891
- title="Failed to load resource details"
892
- details={error.message}
893
- className="my-4"
894
- />
895
- ) : hasExpired ? (
896
- <ExpiredDataMessage />
897
- ) : (
898
- <>
899
- {resolvedAttributes.map((attribute) => (
900
- <AttributeBlock
901
- isLoading={isLoading}
902
- key={attribute}
903
- attribute={attribute}
904
- value={displayData[attribute as keyof typeof displayData]}
905
- context={displayContext}
906
- />
907
- ))}
908
- </>
909
- )}
910
- </div>
929
+ )}
930
+ </div>
931
+ )}
932
+ {error ? (
933
+ <ErrorCard
934
+ title="Failed to load resource details"
935
+ details={error.message}
936
+ className="my-4"
937
+ />
938
+ ) : hasExpired ? (
939
+ <ExpiredDataMessage />
940
+ ) : (
941
+ <>
942
+ {resolvedAttributes.map((attribute) => (
943
+ <AttributeBlock
944
+ isLoading={isLoading}
945
+ key={attribute}
946
+ attribute={attribute}
947
+ value={displayData[attribute as keyof typeof displayData]}
948
+ context={displayContext}
949
+ />
950
+ ))}
951
+ </>
952
+ )}
953
+ </div>
954
+ </DecryptClickContext.Provider>
911
955
  </StreamClickContext.Provider>
912
956
  );
913
957
  };
@@ -67,6 +67,7 @@ export function EntityDetailPanel({
67
67
  onDecrypt,
68
68
  isDecrypting = false,
69
69
  selectedSpan,
70
+ hasEncryptedData = false,
70
71
  }: {
71
72
  run: WorkflowRun;
72
73
  /** Callback when a stream reference is clicked */
@@ -103,6 +104,8 @@ export function EntityDetailPanel({
103
104
  isDecrypting?: boolean;
104
105
  /** Info about the currently selected span from the trace viewer */
105
106
  selectedSpan: SelectedSpanInfo | null;
107
+ /** Run-level hint: the run contains encrypted data (from probe). */
108
+ hasEncryptedData?: boolean;
106
109
  }): React.JSX.Element | null {
107
110
  const toast = useToast();
108
111
  const [stoppingSleep, setStoppingSleep] = useState(false);
@@ -391,13 +394,14 @@ export function EntityDetailPanel({
391
394
  {resourceId}
392
395
  </p>
393
396
  </div>
394
- {(hasEncryptedFields || encryptionKey) && onDecrypt && (
395
- <DecryptButton
396
- decrypted={!!encryptionKey}
397
- loading={isDecrypting}
398
- onClick={onDecrypt}
399
- />
400
- )}
397
+ {(hasEncryptedFields || hasEncryptedData || encryptionKey) &&
398
+ onDecrypt && (
399
+ <DecryptButton
400
+ decrypted={!!encryptionKey}
401
+ loading={isDecrypting}
402
+ onClick={onDecrypt}
403
+ />
404
+ )}
401
405
  </div>
402
406
  </div>
403
407
 
@@ -481,6 +485,8 @@ export function EntityDetailPanel({
481
485
  isLoading={loading}
482
486
  error={error ?? undefined}
483
487
  onStreamClick={onStreamClick}
488
+ onDecrypt={onDecrypt}
489
+ isDecrypting={isDecrypting}
484
490
  resource={resource}
485
491
  />
486
492
  </section>
@@ -75,6 +75,76 @@ export const StreamClickContext = createContext<
75
75
  ((streamId: string) => void) | undefined
76
76
  >(undefined);
77
77
 
78
+ /**
79
+ * Context for passing a decrypt handler down to DataInspector instances.
80
+ * When provided, encrypted markers become clickable buttons that trigger decryption.
81
+ */
82
+ export type DecryptClickContextValue = {
83
+ onDecrypt: () => void;
84
+ isDecrypting: boolean;
85
+ };
86
+
87
+ export const DecryptClickContext = createContext<
88
+ DecryptClickContextValue | undefined
89
+ >(undefined);
90
+
91
+ function EncryptedInlineLabel() {
92
+ const ctx = useContext(DecryptClickContext);
93
+ if (ctx) {
94
+ return (
95
+ <button
96
+ type="button"
97
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] cursor-pointer"
98
+ style={{
99
+ backgroundColor: 'var(--ds-gray-100)',
100
+ color: 'var(--ds-gray-700)',
101
+ border: '1px solid var(--ds-gray-400)',
102
+ fontStyle: 'italic',
103
+ opacity: ctx.isDecrypting ? 0.6 : 1,
104
+ }}
105
+ disabled={ctx.isDecrypting}
106
+ onClick={(e) => {
107
+ e.stopPropagation();
108
+ ctx.onDecrypt();
109
+ }}
110
+ title="Click to decrypt"
111
+ >
112
+ {ctx.isDecrypting ? (
113
+ <span
114
+ className="h-3 w-3 animate-spin rounded-full border-2"
115
+ style={{
116
+ display: 'inline-block',
117
+ flexShrink: 0,
118
+ borderColor: 'var(--ds-gray-400)',
119
+ borderTopColor: 'var(--ds-gray-700)',
120
+ }}
121
+ />
122
+ ) : (
123
+ <Lock
124
+ className="h-3 w-3"
125
+ style={{ display: 'inline', flexShrink: 0 }}
126
+ />
127
+ )}
128
+ <span>{ctx.isDecrypting ? 'Decrypting…' : 'Decrypt'}</span>
129
+ </button>
130
+ );
131
+ }
132
+ return (
133
+ <span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
134
+ <Lock
135
+ className="h-3 w-3"
136
+ style={{
137
+ display: 'inline',
138
+ verticalAlign: 'middle',
139
+ marginRight: '3px',
140
+ marginTop: '-1px',
141
+ }}
142
+ />
143
+ Encrypted
144
+ </span>
145
+ );
146
+ }
147
+
78
148
  function StreamRefInline({ streamRef }: { streamRef: StreamRef }) {
79
149
  const onStreamClick = useContext(StreamClickContext);
80
150
  return (
@@ -132,26 +202,13 @@ function NodeRenderer({
132
202
  }) {
133
203
  const extendedTheme = useContext(ExtendedThemeContext);
134
204
 
135
- // Encrypted marker → flat label with Lock icon, non-expandable
205
+ // Encrypted marker → flat label with Lock icon, clickable when onDecrypt is available
136
206
  if (
137
207
  data !== null &&
138
208
  typeof data === 'object' &&
139
209
  data.constructor?.name === ENCRYPTED_DISPLAY_NAME
140
210
  ) {
141
- const label = (
142
- <span style={{ color: 'var(--ds-gray-600)', fontStyle: 'italic' }}>
143
- <Lock
144
- className="h-3 w-3"
145
- style={{
146
- display: 'inline',
147
- verticalAlign: 'middle',
148
- marginRight: '3px',
149
- marginTop: '-1px',
150
- }}
151
- />
152
- Encrypted
153
- </span>
154
- );
211
+ const label = <EncryptedInlineLabel />;
155
212
  if (depth === 0) {
156
213
  return label;
157
214
  }
@@ -234,6 +291,10 @@ export interface DataInspectorProps {
234
291
  name?: string;
235
292
  /** Callback when a stream reference is clicked */
236
293
  onStreamClick?: (streamId: string) => void;
294
+ /** Callback when an encrypted marker is clicked (triggers decryption) */
295
+ onDecrypt?: () => void;
296
+ /** Whether decryption is currently in progress */
297
+ isDecrypting?: boolean;
237
298
  }
238
299
 
239
300
  export function DataInspector({
@@ -241,6 +302,8 @@ export function DataInspector({
241
302
  expandLevel = 2,
242
303
  name,
243
304
  onStreamClick,
305
+ onDecrypt,
306
+ isDecrypting = false,
244
307
  }: DataInspectorProps) {
245
308
  const stableData = useStableInspectorData(data);
246
309
  const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel);
@@ -269,16 +332,25 @@ export function DataInspector({
269
332
  </ExtendedThemeContext.Provider>
270
333
  );
271
334
 
272
- // Wrap in StreamClickContext if a handler is provided
335
+ let wrapped = content;
336
+
273
337
  if (onStreamClick) {
274
- return (
338
+ wrapped = (
275
339
  <StreamClickContext.Provider value={onStreamClick}>
276
- {content}
340
+ {wrapped}
277
341
  </StreamClickContext.Provider>
278
342
  );
279
343
  }
280
344
 
281
- return content;
345
+ if (onDecrypt) {
346
+ wrapped = (
347
+ <DecryptClickContext.Provider value={{ onDecrypt, isDecrypting }}>
348
+ {wrapped}
349
+ </DecryptClickContext.Provider>
350
+ );
351
+ }
352
+
353
+ return wrapped;
282
354
  }
283
355
 
284
356
  function useStableInspectorData<T>(next: T): T {
@@ -775,6 +775,7 @@ export const WorkflowTraceViewer = ({
775
775
  encryptionKey,
776
776
  onDecrypt,
777
777
  isDecrypting = false,
778
+ hasEncryptedData = false,
778
779
  }: {
779
780
  run: WorkflowRun;
780
781
  events: Event[];
@@ -815,6 +816,8 @@ export const WorkflowTraceViewer = ({
815
816
  onDecrypt?: () => void;
816
817
  /** Whether the encryption key is currently being fetched */
817
818
  isDecrypting?: boolean;
819
+ /** Run-level hint: the run contains encrypted data (from probe). */
820
+ hasEncryptedData?: boolean;
818
821
  }) => {
819
822
  const toast = useToast();
820
823
  const [selectedSpan, setSelectedSpan] = useState<SelectedSpanInfo | null>(
@@ -1151,6 +1154,7 @@ export const WorkflowTraceViewer = ({
1151
1154
  onDecrypt={onDecrypt}
1152
1155
  isDecrypting={isDecrypting}
1153
1156
  selectedSpan={selectedSpan}
1157
+ hasEncryptedData={hasEncryptedData}
1154
1158
  />
1155
1159
  </ErrorBoundary>
1156
1160
  </div>
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  ENCRYPTED_PLACEHOLDER,
41
41
  extractStreamIds,
42
42
  getWebRevivers,
43
+ hasEncryptedFields,
43
44
  hydrateResourceIO,
44
45
  hydrateResourceIOWithKey,
45
46
  isClassInstanceRef,
@@ -359,3 +359,29 @@ export async function hydrateResourceIOWithKey<T>(
359
359
 
360
360
  return result as T;
361
361
  }
362
+
363
+ /**
364
+ * Check whether a hydrated resource (event, step, run, etc.) contains any
365
+ * encrypted display markers. Inspects the standard top-level fields
366
+ * (`input`, `output`, `error`, `metadata`) as well as the event-type-specific
367
+ * `eventData` ref fields.
368
+ */
369
+ export function hasEncryptedFields(resource: unknown): boolean {
370
+ if (!resource || typeof resource !== 'object') return false;
371
+ const r = resource as Record<string, unknown>;
372
+
373
+ for (const key of ['input', 'output', 'metadata', 'error']) {
374
+ if (isEncryptedMarker(r[key])) return true;
375
+ }
376
+
377
+ if (r.eventData && typeof r.eventData === 'object') {
378
+ const eventType = typeof r.eventType === 'string' ? r.eventType : '';
379
+ const refKeys = EVENT_DATA_REF_FIELDS[eventType] ?? [];
380
+ const ed = r.eventData as Record<string, unknown>;
381
+ for (const key of refKeys) {
382
+ if (key in ed && isEncryptedMarker(ed[key])) return true;
383
+ }
384
+ }
385
+
386
+ return false;
387
+ }