opencode-codex-security 0.0.0-stage → 0.1.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 (42) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/DISCLOSURE +19 -0
  3. package/LICENSE +202 -0
  4. package/README.md +103 -2
  5. package/SECURITY.md +22 -0
  6. package/dist/gui-experimental.cjs +17 -0
  7. package/docs/GLOBAL-CONFIG.md +64 -0
  8. package/docs/MULTI-SCAN.md +62 -0
  9. package/docs/PUBLISHING.md +71 -0
  10. package/docs/RELEASING.md +53 -0
  11. package/docs/SDK.md +36 -0
  12. package/fixtures/vulnerable/docs/ARCHITECTURE.md +2 -0
  13. package/fixtures/vulnerable/src/accounts.js +4 -0
  14. package/fixtures/vulnerable/src/diagnostics.js +5 -0
  15. package/fixtures/vulnerable/src/download.js +5 -0
  16. package/fixtures/vulnerable/src/profile.js +4 -0
  17. package/package.json +79 -4
  18. package/src/core/codex-adapter.ts +430 -0
  19. package/src/core/credential-guard.ts +83 -0
  20. package/src/core/planned-scan.ts +316 -0
  21. package/src/core/report-safety.ts +41 -0
  22. package/src/core/result-summary.ts +512 -0
  23. package/src/core/run-history.ts +107 -0
  24. package/src/core/run-telemetry.ts +359 -0
  25. package/src/core/scan-plan.ts +182 -0
  26. package/src/core/scan-service.mjs +129 -0
  27. package/src/core/scanner.mjs +129 -0
  28. package/src/core/sdk-settings.ts +123 -0
  29. package/src/core/security-operations.ts +598 -0
  30. package/src/gui.tsx +102 -0
  31. package/src/operation-tools.ts +247 -0
  32. package/src/plan-view.ts +47 -0
  33. package/src/plugin.ts +346 -0
  34. package/src/rpc.ts +124 -0
  35. package/src/scan-display.ts +284 -0
  36. package/src/security-operations-panel.tsx +551 -0
  37. package/src/security-panel.tsx +1003 -0
  38. package/src/security-styles.ts +10 -0
  39. package/src/security-tui-operations.tsx +343 -0
  40. package/src/state-order.ts +21 -0
  41. package/src/tui.tsx +630 -0
  42. package/src/types.ts +84 -0
@@ -0,0 +1,430 @@
1
+ import {
2
+ costSummary,
3
+ count,
4
+ emptyTelemetry,
5
+ lifecycleSummary,
6
+ measurementSummary,
7
+ phaseSummary,
8
+ recordActivity,
9
+ } from './run-telemetry.ts';
10
+ import { summarizeScanResult } from './result-summary.ts';
11
+ import {
12
+ requireDedicatedSecurityCredentials,
13
+ DedicatedSecurityCredentialsRequiredError,
14
+ } from './credential-guard.ts';
15
+ import { safeFinding } from './report-safety.ts';
16
+ import { randomUUID } from 'node:crypto';
17
+ import { basename, resolve } from 'node:path';
18
+ import type {
19
+ CodexSecurityConfig,
20
+ CodexSecurity,
21
+ ScanOptions,
22
+ ScanResult,
23
+ Finding as SdkFinding,
24
+ } from '@openai/codex-security';
25
+ import type { ResolvedSdkSettings } from './sdk-settings.ts';
26
+ import type { Finding, ScanState } from '../types.js';
27
+
28
+ /** Per-run consent, never persisted. The caller must obtain this from the user. */
29
+ export interface CodexScanApproval {
30
+ approved: true;
31
+ repository: string;
32
+ allowNetworkModelInference: true;
33
+ maxCostUsd: number;
34
+ auth?: 'chatgpt' | 'api-key';
35
+ settings?: ResolvedSdkSettings;
36
+ scopePaths?: string[];
37
+ scanPrompt?: string;
38
+ contextFiles?: string[];
39
+ }
40
+ export type SecurityClient = Pick<CodexSecurity, 'run' | 'close'>;
41
+ export type SecurityFactory = (
42
+ config?: CodexSecurityConfig,
43
+ ) => SecurityClient | Promise<SecurityClient>;
44
+ const realClient: SecurityFactory = async (config) => {
45
+ await requireDedicatedSecurityCredentials();
46
+ const { CodexSecurity } = await import('@openai/codex-security');
47
+ return new CodexSecurity(config ?? { codexOverrides: { model_provider: 'openai' } });
48
+ };
49
+ function initial(repository: string, revision = 0, instanceId = ''): ScanState {
50
+ return {
51
+ telemetry: emptyTelemetry(),
52
+ instanceId,
53
+ status: 'idle',
54
+ scanId: null,
55
+ phase: 'Awaiting per-scan approval',
56
+ progress: { done: 0, total: 0 },
57
+ findings: [],
58
+ events: [],
59
+ error: null,
60
+ startedAt: null,
61
+ finishedAt: null,
62
+ repository,
63
+ engine: 'codex-security-sdk',
64
+ skipped: 0,
65
+ revision,
66
+ };
67
+ }
68
+ export function mapSdkFinding(finding: SdkFinding): Finding | null {
69
+ // UI contract has four severities. Never silently upgrade informational items.
70
+ if (finding.severity.level === 'informational') return null;
71
+ const location = finding.locations[0];
72
+ return safeFinding({
73
+ id: finding.occurrenceId || finding.findingId,
74
+ title: finding.title,
75
+ severity: finding.severity.level,
76
+ path: location?.path ?? '',
77
+ line: location?.startLine ?? 0,
78
+ cwe: finding.taxonomy.cwe.join(', '),
79
+ summary: finding.summary,
80
+ evidence: (finding.codeEvidence ?? finding.code_evidence ?? [])
81
+ .map((item) => item.code)
82
+ .join('\n\n'),
83
+ remediation: finding.remediation,
84
+ confidence: finding.confidence.level,
85
+ });
86
+ }
87
+
88
+ /** Whitelist numeric budget diagnostics; never surface the SDK's raw error text or artifact paths. */
89
+ export function scanBudgetFailure(
90
+ error: unknown,
91
+ ): { message: string; estimatedCostUsd: number } | undefined {
92
+ if (!(error instanceof Error) || error.name !== 'ScanCostLimitExceededError') return;
93
+ const value = error as Error & {
94
+ maxCostUsd?: number;
95
+ cost?: { estimatedUsd?: number; estimatedUsdRange?: { min?: number; max?: number } };
96
+ };
97
+ const estimate = value.cost?.estimatedUsd,
98
+ limit = value.maxCostUsd;
99
+ if (
100
+ typeof estimate !== 'number' ||
101
+ !Number.isFinite(estimate) ||
102
+ estimate < 0 ||
103
+ typeof limit !== 'number' ||
104
+ !Number.isFinite(limit) ||
105
+ limit <= 0
106
+ )
107
+ return;
108
+ const range = value.cost?.estimatedUsdRange;
109
+ const detail =
110
+ typeof range?.min === 'number' &&
111
+ Number.isFinite(range.min) &&
112
+ typeof range.max === 'number' &&
113
+ Number.isFinite(range.max) &&
114
+ range.min >= 0 &&
115
+ range.max >= range.min
116
+ ? ` Estimated range: USD ${range.min.toFixed(2)} to ${range.max.toFixed(2)}.`
117
+ : '';
118
+ return {
119
+ estimatedCostUsd: estimate,
120
+ message: `Stopped at the estimated cost guard: USD ${estimate.toFixed(2)} baseline exceeded the USD ${limit.toFixed(2)} limit.${detail} This is an estimate, not a billing total. No completed assessment; review the budget before choosing whether to retry.`,
121
+ };
122
+ }
123
+
124
+ /** Local coordinator; SDK inference is network-backed, not an offline scanner. */
125
+ export class CodexScanService {
126
+ readonly root: string;
127
+ #instanceId = randomUUID();
128
+ #factory: SecurityFactory;
129
+ #state: ScanState;
130
+ #listeners = new Set<(state: ScanState) => void>();
131
+ #controller?: AbortController;
132
+ #task?: Promise<void>;
133
+ #disposed = false;
134
+ #busy = false;
135
+ constructor(
136
+ root: string,
137
+ { createClient = realClient }: { createClient?: SecurityFactory } = {},
138
+ ) {
139
+ this.root = resolve(root);
140
+ this.#factory = createClient;
141
+ this.#state = initial(basename(this.root), 0, this.#instanceId);
142
+ }
143
+ getState(): ScanState {
144
+ return structuredClone(this.#state);
145
+ }
146
+ subscribe(listener: (state: ScanState) => void) {
147
+ this.#listeners.add(listener);
148
+ return () => this.#listeners.delete(listener);
149
+ }
150
+ #emit(message?: string) {
151
+ if (message) this.#state.events.push({ time: new Date().toISOString(), message });
152
+ this.#state.events = this.#state.events.slice(-100);
153
+ this.#state.updatedAt = new Date().toISOString();
154
+ this.#state.revision++;
155
+ for (const listener of this.#listeners) {
156
+ try {
157
+ listener(this.getState());
158
+ } catch {
159
+ /* View isolation. */
160
+ }
161
+ }
162
+ }
163
+ start(approval?: CodexScanApproval): ScanState {
164
+ if (this.#disposed) throw new Error('Scan service is closed');
165
+ if (this.#busy) return this.getState();
166
+ if (
167
+ !approval ||
168
+ approval.approved !== true ||
169
+ approval.allowNetworkModelInference !== true ||
170
+ approval.repository !== this.root ||
171
+ !Number.isFinite(approval.maxCostUsd) ||
172
+ approval.maxCostUsd <= 0
173
+ ) {
174
+ throw new Error(
175
+ 'Per-scan approval required for this exact repository, network model inference, and positive estimated USD cost limit.',
176
+ );
177
+ }
178
+ this.#state = {
179
+ ...initial(basename(this.root), this.#state.revision, this.#instanceId),
180
+ status: 'running',
181
+ scanId: null,
182
+ startedAt: new Date().toISOString(),
183
+ phase: 'Starting Codex Security',
184
+ };
185
+ this.#busy = true;
186
+ this.#controller = new AbortController();
187
+ this.#emit(
188
+ 'Approved SDK scan started. Local runner and artifacts; network model inference. Cost limit is estimated, not a guaranteed billing cap.',
189
+ );
190
+ this.#task = this.#run(structuredClone(approval), this.#controller.signal);
191
+ return this.getState();
192
+ }
193
+ async #run(approval: CodexScanApproval, signal: AbortSignal) {
194
+ let client: SecurityClient | undefined;
195
+ let observing = true;
196
+ let outcome: 'completed' | 'cancelled' | 'error' = 'error';
197
+ let terminalMessage = '';
198
+ let terminalPhase = 'Codex Security scan failed';
199
+ try {
200
+ signal.throwIfAborted();
201
+ client = await this.#factory({
202
+ codexOverrides: {
203
+ model_provider: 'openai',
204
+ ...(approval.settings
205
+ ? {
206
+ model: approval.settings.codex.model,
207
+ model_reasoning_effort: approval.settings.codex.reasoningEffort,
208
+ daybreak: approval.settings.codex.daybreak,
209
+ }
210
+ : {}),
211
+ },
212
+ });
213
+ signal.throwIfAborted();
214
+ const telemetry = (this.#state.telemetry ??= emptyTelemetry());
215
+ const options: ScanOptions = {
216
+ target: approval.scopePaths?.length ? [...approval.scopePaths] : 'repository',
217
+ scanPrompt: approval.scanPrompt ?? '',
218
+ knowledgeBasePaths: [...(approval.contextFiles ?? [])],
219
+ ...approval.settings?.scan,
220
+ mode: approval.settings?.scan.mode ?? 'standard',
221
+ auth: approval.auth ?? 'chatgpt',
222
+ maxCostUsd: approval.maxCostUsd,
223
+ ownerSuggestions: false,
224
+ signal,
225
+ onProgress: (progress) => {
226
+ if (!observing || signal.aborted) return;
227
+ const detail = phaseSummary(progress);
228
+ telemetry.phaseProgress = detail;
229
+ const counts = detail.reviews && detail.reviews.total > 0 ? detail.reviews : detail.files;
230
+ const unit = detail.reviews && detail.reviews.total > 0 ? 'reviews' : 'files';
231
+ this.#state.progress = { ...counts, unit };
232
+ const phase =
233
+ telemetry.lifecycle?.status === 'completed'
234
+ ? 'Finishing SDK result'
235
+ : `${detail.phase.replaceAll('_', ' ')} (${unit})`;
236
+ const changed = phase !== this.#state.phase;
237
+ this.#state.phase = phase;
238
+ this.#emit(changed ? `Phase: ${phase}` : undefined);
239
+ },
240
+ onMeasurement: (measurement) => {
241
+ if (!observing || signal.aborted) return;
242
+ telemetry.measurement = measurementSummary(measurement);
243
+ if (telemetry.measurement.cost)
244
+ this.#state.estimatedCostUsd = telemetry.measurement.cost.baselineUsd;
245
+ else delete this.#state.estimatedCostUsd;
246
+ this.#emit();
247
+ },
248
+ onCost: (cost) => {
249
+ if (!observing || signal.aborted) return;
250
+ // onMeasurement carries coverage/timestamps. Never replace it with a thinner callback.
251
+ if (!telemetry.measurement) {
252
+ const summary = costSummary(cost);
253
+ if (summary) {
254
+ telemetry.measurement = {
255
+ coverage: 'complete',
256
+ observedAt: new Date().toISOString(),
257
+ cost: summary,
258
+ };
259
+ this.#state.estimatedCostUsd = summary.baselineUsd;
260
+ this.#emit();
261
+ }
262
+ }
263
+ },
264
+ onWorkerEvent: (worker) => {
265
+ if (!observing || signal.aborted || !Number.isInteger(worker.worker) || worker.worker < 0)
266
+ return;
267
+ if (
268
+ !telemetry.observedWorkers.includes(worker.worker) &&
269
+ telemetry.observedWorkers.length < 128
270
+ ) {
271
+ telemetry.observedWorkers.push(worker.worker);
272
+ this.#emit(`Worker ${worker.worker} session observed`);
273
+ }
274
+ },
275
+ onReconnect: (attempt, maxAttempts, details) => {
276
+ if (!observing || signal.aborted) return;
277
+ telemetry.lastReconnect = {
278
+ attempt: count(attempt),
279
+ maxAttempts: count(maxAttempts),
280
+ at: new Date().toISOString(),
281
+ ...(details &&
282
+ ['rate_limit', 'network', 'authentication', 'authorization'].includes(details.reason)
283
+ ? { reason: details.reason }
284
+ : {}),
285
+ ...(details &&
286
+ Number.isFinite(details.retryAfterSeconds) &&
287
+ details.retryAfterSeconds! >= 0
288
+ ? { retryAfterSeconds: details.retryAfterSeconds }
289
+ : {}),
290
+ };
291
+ this.#emit(`SDK reconnect attempt ${count(attempt)} of ${count(maxAttempts)}`);
292
+ },
293
+ onWorkerStatus: (worker) => {
294
+ if (observing && !signal.aborted) {
295
+ this.#state.workers = {
296
+ phase: ['ranking', 'file_review', 'validation', 'attack_path'].includes(worker.phase)
297
+ ? worker.phase
298
+ : 'unknown',
299
+ planned: count(worker.planned),
300
+ started: count(worker.started),
301
+ };
302
+ this.#emit();
303
+ }
304
+ },
305
+ onDeepProgress: (progress) => {
306
+ if (observing && !signal.aborted) {
307
+ this.#state.deepProgress = {
308
+ completed: count(progress.completed),
309
+ active: count(progress.active),
310
+ maximum: count(progress.maximum),
311
+ consolidating: progress.consolidating === true,
312
+ };
313
+ this.#emit();
314
+ }
315
+ },
316
+ onLifecycle: (lifecycle) => {
317
+ if (observing && !signal.aborted) {
318
+ this.#state.scanId = lifecycle.scanId;
319
+ telemetry.lifecycle = lifecycleSummary(lifecycle);
320
+ if (telemetry.lifecycle?.status === 'completed')
321
+ this.#state.phase = 'Finishing SDK result';
322
+ this.#emit(
323
+ telemetry.lifecycle?.status === 'completed'
324
+ ? 'SDK core scan completed; waiting for the final result. This is not a deduplication progress signal.'
325
+ : undefined,
326
+ );
327
+ }
328
+ },
329
+ // Avoid exposing raw model messages, command arguments, auth details, or tool output in UI events.
330
+ onActivity: (activity) => {
331
+ if (observing && !signal.aborted) {
332
+ recordActivity(telemetry, activity, this.root);
333
+ this.#emit();
334
+ }
335
+ },
336
+ onWarning: () => {
337
+ if (observing && !signal.aborted)
338
+ this.#emit(
339
+ 'SDK reported a warning; inspect local SDK artifacts before relying on results.',
340
+ );
341
+ },
342
+ };
343
+ const result: ScanResult = await client.run(this.root, options);
344
+ signal.throwIfAborted();
345
+ this.#state.findings = result.findings.findings.flatMap((finding) => {
346
+ const mapped = mapSdkFinding(finding);
347
+ if (!mapped) this.#state.skipped++;
348
+ return mapped ? [mapped] : [];
349
+ });
350
+ this.#state.scanId = result.findings.scanId;
351
+ this.#state.assessment = summarizeScanResult(result, this.root);
352
+ if (!telemetry.measurement && result.cost) {
353
+ telemetry.measurement = {
354
+ coverage: 'unavailable',
355
+ observedAt: new Date().toISOString(),
356
+ cost: costSummary(result.cost),
357
+ };
358
+ this.#state.estimatedCostUsd = telemetry.measurement.cost?.baselineUsd;
359
+ }
360
+ outcome = 'completed';
361
+ terminalPhase = 'Codex Security scan complete';
362
+ terminalMessage = `Completed with ${this.#state.findings.length} findings; ${this.#state.skipped} informational items omitted from this four-severity view. Review full local SDK artifacts. Findings are available at completion, not streamed.`;
363
+ } catch (error) {
364
+ outcome = signal.aborted ? 'cancelled' : 'error';
365
+ terminalPhase = signal.aborted
366
+ ? 'Cancelled; results incomplete'
367
+ : 'Codex Security scan failed';
368
+ // SDK errors can contain source, credentials, or command output. Do not send raw details to views.
369
+ const budget = signal.aborted ? undefined : scanBudgetFailure(error);
370
+ if (budget) {
371
+ this.#state.estimatedCostUsd = budget.estimatedCostUsd;
372
+ const cost = costSummary((error as { cost: Parameters<typeof costSummary>[0] }).cost);
373
+ (this.#state.telemetry ??= emptyTelemetry()).measurement = {
374
+ coverage: 'partial',
375
+ observedAt: new Date().toISOString(),
376
+ cost,
377
+ };
378
+ terminalPhase = 'Stopped at estimated cost guard';
379
+ }
380
+ this.#state.error = signal.aborted
381
+ ? null
382
+ : (budget?.message ??
383
+ (error instanceof DedicatedSecurityCredentialsRequiredError
384
+ ? error.message
385
+ : 'SDK scan failed. Check authentication, runtime prerequisites, and local SDK diagnostics.'));
386
+ terminalMessage = signal.aborted
387
+ ? 'Cancellation requested; SDK run stopped. Partial results are not a completed scan.'
388
+ : this.#state.error!;
389
+ } finally {
390
+ observing = false;
391
+ this.#state.phase = 'Closing Codex Security runtime';
392
+ this.#emit();
393
+ try {
394
+ await client?.close();
395
+ } catch {
396
+ this.#emit('SDK cleanup failed; inspect local runtime.');
397
+ }
398
+ if (signal.aborted) {
399
+ outcome = 'cancelled';
400
+ terminalPhase = 'Cancelled; results incomplete';
401
+ terminalMessage =
402
+ 'Cancellation requested; SDK run stopped. Partial results are not a completed scan.';
403
+ this.#state.error = null;
404
+ }
405
+ this.#state.status = outcome;
406
+ this.#state.phase = terminalPhase;
407
+ this.#busy = false;
408
+ this.#state.finishedAt = new Date().toISOString();
409
+ this.#emit(terminalMessage);
410
+ }
411
+ }
412
+ cancel(): ScanState {
413
+ if (this.#state.status === 'running' && !this.#controller?.signal.aborted) {
414
+ this.#controller?.abort();
415
+ this.#state.phase = 'Cancellation requested';
416
+ this.#emit('Cancellation requested; waiting for the runtime to stop.');
417
+ }
418
+ return this.getState();
419
+ }
420
+ async wait(): Promise<ScanState> {
421
+ await this.#task;
422
+ return this.getState();
423
+ }
424
+ async dispose() {
425
+ this.#disposed = true;
426
+ this.cancel();
427
+ await this.#task;
428
+ this.#listeners.clear();
429
+ }
430
+ }
@@ -0,0 +1,83 @@
1
+ import { lstat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+
5
+ type Environment = Record<string, string | undefined>;
6
+
7
+ /** Fixed, non-sensitive guidance is safe for UI/model errors. Never attach filesystem errors. */
8
+ export class DedicatedSecurityCredentialsRequiredError extends Error {
9
+ constructor() {
10
+ super(
11
+ 'Dedicated Codex Security authentication is missing or is not private and safe. Set it up explicitly before retrying. This plugin will not initialize authentication or import ambient credentials.',
12
+ );
13
+ this.name = 'DedicatedSecurityCredentialsRequiredError';
14
+ }
15
+ }
16
+
17
+ function environmentValue(environment: Environment, requested: string, preserveWhitespace = false) {
18
+ const exact = environment[requested];
19
+ const value = exact?.trim()
20
+ ? exact
21
+ : Object.entries(environment).find(
22
+ ([name, entry]) => name.toUpperCase() === requested && entry?.trim(),
23
+ )?.[1];
24
+ return preserveWhitespace ? value : value?.trim();
25
+ }
26
+
27
+ /** Pure equivalent of SDK 0.9.1 runtime path selection; no credential or SDK access. */
28
+ export function dedicatedSecurityAuthPath(
29
+ environment: Environment = process.env,
30
+ defaultHome = homedir(),
31
+ ): string {
32
+ const expandHome = (value: string) => {
33
+ const home =
34
+ (process.platform === 'win32'
35
+ ? (environmentValue(environment, 'USERPROFILE') ?? environmentValue(environment, 'HOME'))
36
+ : (environmentValue(environment, 'HOME') ??
37
+ environmentValue(environment, 'USERPROFILE'))) ?? defaultHome;
38
+ if (value === '~') return home;
39
+ if (value.startsWith('~/')) return join(home, value.slice(2));
40
+ if (value.startsWith('~\\')) return join(home, ...value.slice(2).split('\\'));
41
+ return value;
42
+ };
43
+ const configured = environmentValue(environment, 'CODEX_SECURITY_STATE_DIR');
44
+ const stateDirectory =
45
+ configured !== undefined
46
+ ? resolve(expandHome(configured))
47
+ : resolve(
48
+ expandHome(
49
+ environmentValue(environment, 'CODEX_HOME', true) ?? join(defaultHome, '.codex'),
50
+ ),
51
+ 'state',
52
+ 'plugins',
53
+ 'codex-security',
54
+ );
55
+ return join(stateDirectory, 'codex-home', 'auth.json');
56
+ }
57
+
58
+ /**
59
+ * Read metadata only. Do not call account/login/prepare/import helpers: SDK 0.9.1
60
+ * otherwise copies ambient auth when the dedicated auth file is absent.
61
+ * This is a precondition, not a token-validity check or an atomic SDK auth policy.
62
+ */
63
+ export async function requireDedicatedSecurityCredentials(
64
+ environment: Environment = process.env,
65
+ ): Promise<void> {
66
+ try {
67
+ // POSIX mode bits cannot establish Windows ACL privacy. Fail closed rather
68
+ // than invoke the SDK's ACL helper, which can change security permissions.
69
+ if (process.platform === 'win32') throw new DedicatedSecurityCredentialsRequiredError();
70
+ const path = dedicatedSecurityAuthPath(environment);
71
+ const home = await lstat(dirname(path));
72
+ const auth = await lstat(path);
73
+ const uid = process.geteuid?.() ?? process.getuid?.();
74
+ if (!home.isDirectory() || home.isSymbolicLink() || !auth.isFile() || auth.isSymbolicLink())
75
+ throw new DedicatedSecurityCredentialsRequiredError();
76
+ if ((home.mode & 0o077) !== 0 || (auth.mode & 0o077) !== 0)
77
+ throw new DedicatedSecurityCredentialsRequiredError();
78
+ if (uid !== undefined && (home.uid !== uid || auth.uid !== uid))
79
+ throw new DedicatedSecurityCredentialsRequiredError();
80
+ } catch {
81
+ throw new DedicatedSecurityCredentialsRequiredError();
82
+ }
83
+ }