@manukyalo/scopelock 2.2.0 → 3.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.
package/src/blast.js ADDED
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * src/blast.js
5
+ *
6
+ * Cross-File Blast Radius Map.
7
+ *
8
+ * Given a file path, scan the entire repo for any file that imports or
9
+ * requires it. Returns an array of dependent file paths.
10
+ *
11
+ * Handles the following import patterns (JS/TS/Python/Go):
12
+ * import ... from './path/to/file'
13
+ * require('./path/to/file')
14
+ * from './path/to/file' import ... (Python)
15
+ * import "./path/to/file"
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // Directories that are never part of the blast radius analysis.
22
+ const IGNORED_DIRS = new Set([
23
+ 'node_modules', '.git', '.next', 'dist', 'build', 'out', 'coverage',
24
+ '.turbo', '.cache', '__pycache__', '.venv', 'venv', 'target',
25
+ ]);
26
+
27
+ /**
28
+ * Recursively walk a directory and collect all files.
29
+ * @param {string} dir
30
+ * @returns {string[]}
31
+ */
32
+ function walkDir(dir) {
33
+ const results = [];
34
+ let entries;
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ } catch {
38
+ return results;
39
+ }
40
+ for (const entry of entries) {
41
+ if (entry.name.startsWith('.') && entry.name !== '.') continue;
42
+ if (IGNORED_DIRS.has(entry.name)) continue;
43
+ const full = path.join(dir, entry.name);
44
+ if (entry.isDirectory()) {
45
+ results.push(...walkDir(full));
46
+ } else {
47
+ results.push(full);
48
+ }
49
+ }
50
+ return results;
51
+ }
52
+
53
+ /**
54
+ * Generate all possible import path variants for a given target file.
55
+ * For example, given src/auth/token.ts, this yields:
56
+ * ./token, ../auth/token, ../../src/auth/token, etc.
57
+ * We match against these from each candidate file's directory.
58
+ *
59
+ * @param {string} targetRelative e.g. "src/auth/token.ts"
60
+ * @returns {string[]} stem variants (without extension, for partial matching)
61
+ */
62
+ function getTargetStems(targetRelative) {
63
+ const normalized = targetRelative.replace(/\\/g, '/');
64
+ const noExt = normalized.replace(/\.[^.]+$/, '');
65
+ return [normalized, noExt];
66
+ }
67
+
68
+ /**
69
+ * Check if a file's content contains an import/require of the target.
70
+ *
71
+ * @param {string} fileContent
72
+ * @param {string[]} targetStems
73
+ * @param {string} fileDir Absolute directory of the importing file
74
+ * @param {string} repoRoot Absolute repo root
75
+ * @param {string} targetAbsolute Absolute path of the target file
76
+ * @returns {boolean}
77
+ */
78
+ function fileImportsTarget(fileContent, targetAbsolute, fileDir) {
79
+ // Extract all quoted string literals that look like import paths
80
+ const importPathRe = /(?:from|import|require)\s*\(?['"]([^'"]+)['"]\)?/g;
81
+ let match;
82
+ while ((match = importPathRe.exec(fileContent)) !== null) {
83
+ const importPath = match[1];
84
+ // Only resolve relative paths; skip node_module specifiers
85
+ if (!importPath.startsWith('.')) continue;
86
+ try {
87
+ const resolved = path.resolve(fileDir, importPath);
88
+ // Match with or without extension
89
+ const targetNoExt = targetAbsolute.replace(/\.[^.]+$/, '');
90
+ if (
91
+ resolved === targetAbsolute ||
92
+ resolved === targetNoExt ||
93
+ resolved + path.extname(targetAbsolute) === targetAbsolute
94
+ ) {
95
+ return true;
96
+ }
97
+ } catch {
98
+ // If resolution fails, skip
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+
104
+ /**
105
+ * Compute the full blast radius for a given file.
106
+ *
107
+ * @param {string} targetFile Relative or absolute path to the file.
108
+ * @returns {{ target: string, dependents: string[], total: number }}
109
+ */
110
+ function blastRadius(targetFile) {
111
+ const repoRoot = process.cwd();
112
+ const targetAbs = path.resolve(repoRoot, targetFile);
113
+ const targetRel = path.relative(repoRoot, targetAbs).replace(/\\/g, '/');
114
+ const allFiles = walkDir(repoRoot);
115
+ const dependents = [];
116
+
117
+ for (const f of allFiles) {
118
+ // Skip the target itself
119
+ if (path.resolve(f) === targetAbs) continue;
120
+
121
+ let content;
122
+ try {
123
+ content = fs.readFileSync(f, 'utf8');
124
+ } catch {
125
+ continue;
126
+ }
127
+
128
+ if (fileImportsTarget(content, targetAbs, path.dirname(f))) {
129
+ dependents.push(path.relative(repoRoot, f).replace(/\\/g, '/'));
130
+ }
131
+ }
132
+
133
+ return { target: targetRel, dependents, total: dependents.length };
134
+ }
135
+
136
+ /**
137
+ * Print a human-readable blast radius report to stdout.
138
+ *
139
+ * @param {string} targetFile
140
+ */
141
+ function printBlastRadius(targetFile) {
142
+ const { target, dependents, total } = blastRadius(targetFile);
143
+
144
+ console.log(`\nšŸ’„ Blast Radius: ${target}\n`);
145
+
146
+ if (total === 0) {
147
+ console.log(' No other files import this file. Safe to modify.\n');
148
+ return;
149
+ }
150
+
151
+ console.log(` ${total} file(s) directly import this file:\n`);
152
+ for (const dep of dependents) {
153
+ console.log(` → ${dep}`);
154
+ }
155
+ console.log('');
156
+ console.log(` āš ļø Modifying '${target}' may impact all ${total} of the above file(s).`);
157
+ console.log(` Run 'scopelock lock ${target}' to protect it before your session.\n`);
158
+ }
159
+
160
+ module.exports = { blastRadius, printBlastRadius };
package/src/git.js CHANGED
@@ -1,166 +1,167 @@
1
- 'use strict';
2
-
3
- /**
4
- * src/git.js
5
- *
6
- * Scope violation checker — V2.
7
- *
8
- * Two tiers of enforcement:
9
- * 1. File-level: Any changed file whose manifest status is 'locked' → violation.
10
- * 2. Function-level: Any changed file that has locked functions →
11
- * parse the diff for changed line numbers, re-extract
12
- * function boundaries from the current file, and flag
13
- * any changed line that falls inside a locked function.
14
- *
15
- * Exits 1 if any violation found. Wireable as a pre-commit hook.
16
- */
17
-
18
- const { execSync } = require('child_process');
19
- const { getManifest } = require('./manifest');
20
- const { getChangedLines } = require('./diff');
21
- const { extractFunctions } = require('./parser');
22
- const { detectSecret } = require('./secrets');
23
-
24
- function check(args = []) {
25
- const requireTests = args.includes('--require-tests');
26
- const manifest = getManifest();
27
- let diffOutput;
28
-
29
- try {
30
- diffOutput = execSync('git diff HEAD --name-only', {
31
- encoding: 'utf8',
32
- stdio: ['pipe', 'pipe', 'pipe'],
33
- });
34
- } catch (err) {
35
- console.error('git error — are you inside a git repository with at least one commit?');
36
- console.error(err.message);
37
- process.exit(1);
38
- }
39
-
40
- const changedFiles = diffOutput
41
- .split('\n')
42
- .map(f => f.trim())
43
- .filter(f => f.length > 0);
44
-
45
- if (changedFiles.length === 0) {
46
- console.log('āœ… Scope check passed — no changes detected.');
47
- return;
48
- }
49
-
50
- const violations = [];
51
-
52
- // ── Tier -1: Test Coverage Gate ───────────────────────────────────────────
53
- if (requireTests) {
54
- const hasSourceChanges = changedFiles.some(f =>
55
- !f.includes('.test.') && !f.includes('.spec.') && !f.includes('/test/') && !f.includes('/__tests__/') &&
56
- (f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.py') || f.endsWith('.go') || f.endsWith('.rs'))
57
- );
58
- const hasTestChanges = changedFiles.some(f =>
59
- f.includes('.test.') || f.includes('.spec.') || f.includes('/test/') || f.includes('/__tests__/')
60
- );
61
- if (hasSourceChanges && !hasTestChanges) {
62
- violations.push({
63
- type: 'test-gate',
64
- file: 'N/A',
65
- message: 'TEST GATE VIOLATION: Source logic was modified, but no tests were added or updated. You must write tests to pass `--require-tests`.'
66
- });
67
- }
68
- }
69
-
70
- for (const file of changedFiles) {
71
- const normalizedFile = file.replace(/\\/g, '/');
72
- const entry = manifest.files[normalizedFile];
73
-
74
- const changedLines = getChangedLines(normalizedFile);
75
-
76
- // ── Tier 0: Secret Sentinel ─────────────────────────────────────────────
77
- if (changedLines.size > 0) {
78
- // Check if this file has explicitly allowed secrets
79
- const hasOverride = manifest.allowedSecrets && manifest.allowedSecrets[normalizedFile];
80
- if (!hasOverride) {
81
- for (const [lineNum, content] of changedLines.entries()) {
82
- const secretType = detectSecret(content);
83
- if (secretType) {
84
- violations.push({
85
- type: 'secret',
86
- file: normalizedFile,
87
- message: `SECRET LEAK [${secretType}] detected in '${normalizedFile}' on line ${lineNum}.`,
88
- });
89
- break; // One secret violation per file is enough
90
- }
91
- }
92
- }
93
- }
94
-
95
- // ── Tier 1: File-level lock ─────────────────────────────────────────────
96
- if (entry && entry.status === 'locked') {
97
- violations.push({
98
- type: 'file',
99
- file: normalizedFile,
100
- message: `File '${normalizedFile}' is LOCKED.`,
101
- });
102
- continue; // No need to check functions if the whole file is locked
103
- }
104
-
105
- // ── Tier 2: Function-level lock ─────────────────────────────────────────
106
- if (!entry || !entry.functions) continue;
107
-
108
- const lockedFunctions = Object.entries(entry.functions)
109
- .filter(([, fnData]) => fnData.status === 'locked')
110
- .map(([name]) => name);
111
-
112
- if (lockedFunctions.length === 0) continue;
113
-
114
- // Re-extract function boundaries from the current on-disk file
115
- const currentFunctions = extractFunctions(normalizedFile);
116
-
117
- for (const lockedFnName of lockedFunctions) {
118
- const fn = currentFunctions.find(f => f.name === lockedFnName);
119
- if (!fn) {
120
- // Function was deleted or renamed — this itself is a violation
121
- violations.push({
122
- type: 'function-missing',
123
- file: normalizedFile,
124
- fn: lockedFnName,
125
- message: `Locked function '${lockedFnName}' in '${normalizedFile}' was removed or renamed.`,
126
- });
127
- continue;
128
- }
129
-
130
- // Check if any changed line falls within the function's boundaries
131
- for (const line of changedLines.keys()) {
132
- if (line >= fn.startLine && line <= fn.endLine) {
133
- violations.push({
134
- type: 'function',
135
- file: normalizedFile,
136
- fn: lockedFnName,
137
- message:
138
- `Locked function '${lockedFnName}' in '${normalizedFile}' was modified ` +
139
- `(changed line ${line} is inside [${fn.startLine}–${fn.endLine}]).`,
140
- });
141
- break; // One violation per function is enough
142
- }
143
- }
144
- }
145
- }
146
-
147
- // ── Report ────────────────────────────────────────────────────────────────
148
- if (violations.length === 0) {
149
- console.log('āœ… Scope check passed — no locked files or functions were modified.');
150
- return;
151
- }
152
-
153
- console.error(`\nāŒ Scope violations detected:\n`);
154
- for (const v of violations) {
155
- console.error(` VIOLATION: ${v.message}`);
156
- }
157
- console.error(
158
- `\n${violations.length} violation(s) found.\n` +
159
- ` • Revert unintentional changes with: git restore <file>\n` +
160
- ` • Explicitly unlock with: scopelock unlock <file>[:<function>] "<reason>"`
161
- );
162
-
163
- process.exit(1);
164
- }
165
-
166
- module.exports = { check };
1
+ 'use strict';
2
+
3
+ /**
4
+ * src/git.js
5
+ *
6
+ * Scope violation checker — V2.
7
+ *
8
+ * Two tiers of enforcement:
9
+ * 1. File-level: Any changed file whose manifest status is 'locked' → violation.
10
+ * 2. Function-level: Any changed file that has locked functions →
11
+ * parse the diff for changed line numbers, re-extract
12
+ * function boundaries from the current file, and flag
13
+ * any changed line that falls inside a locked function.
14
+ *
15
+ * Exits 1 if any violation found. Wireable as a pre-commit hook.
16
+ */
17
+
18
+ const { execSync } = require('child_process');
19
+ const { getManifest } = require('./manifest');
20
+ const { getChangedLines } = require('./diff');
21
+ const { extractFunctions } = require('./parser');
22
+ const { detectSecret } = require('./secrets');
23
+
24
+ function guard(args = []) {
25
+ const requireTests = args.includes('--tests');
26
+ const manifest = getManifest();
27
+ let diffOutput;
28
+
29
+ try {
30
+ diffOutput = execSync('git diff HEAD --name-only', {
31
+ encoding: 'utf8',
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ });
34
+ } catch (err) {
35
+ console.error('git error — are you inside a git repository with at least one commit?');
36
+ console.error(err.message);
37
+ process.exit(1);
38
+ }
39
+
40
+ const changedFiles = diffOutput
41
+ .split('\n')
42
+ .map(f => f.trim())
43
+ .filter(f => f.length > 0);
44
+
45
+ if (changedFiles.length === 0) {
46
+ console.log('āœ… Scope guard passed — no changes detected.');
47
+ return;
48
+ }
49
+
50
+ const violations = [];
51
+
52
+ // ── Tier -1: Test Coverage Gate ───────────────────────────────────────────
53
+ if (requireTests) {
54
+ const hasSourceChanges = changedFiles.some(f =>
55
+ !f.includes('.test.') && !f.includes('.spec.') && !f.includes('/test/') && !f.includes('/__tests__/') &&
56
+ (f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.py') || f.endsWith('.go') || f.endsWith('.rs'))
57
+ );
58
+ const hasTestChanges = changedFiles.some(f =>
59
+ f.includes('.test.') || f.includes('.spec.') || f.includes('/test/') || f.includes('/__tests__/')
60
+ );
61
+ if (hasSourceChanges && !hasTestChanges) {
62
+ violations.push({
63
+ type: 'test-gate',
64
+ file: 'N/A',
65
+ message: 'TEST GATE VIOLATION: Source logic was modified, but no tests were added or updated. You must write tests to pass `--tests`.'
66
+ });
67
+ }
68
+ }
69
+
70
+ for (const file of changedFiles) {
71
+ const normalizedFile = file.replace(/\\/g, '/');
72
+ const entry = manifest.files[normalizedFile];
73
+
74
+ const changedLines = getChangedLines(normalizedFile);
75
+
76
+ // ── Tier 0: Secret Sentinel ─────────────────────────────────────────────
77
+ if (changedLines.size > 0) {
78
+ // Check if this file has explicitly allowed secrets
79
+ const hasOverride = manifest.allowedSecrets && manifest.allowedSecrets[normalizedFile];
80
+ if (!hasOverride) {
81
+ for (const [lineNum, content] of changedLines.entries()) {
82
+ const secretType = detectSecret(content);
83
+ if (secretType) {
84
+ violations.push({
85
+ type: 'secret',
86
+ file: normalizedFile,
87
+ message: `SECRET LEAK [${secretType}] detected in '${normalizedFile}' on line ${lineNum}.`,
88
+ });
89
+ break; // One secret violation per file is enough
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ // ── Tier 1: File-level lock ─────────────────────────────────────────────
96
+ if (entry && (entry.status === 'locked' || entry.status === 'sealed')) {
97
+ const label = entry.status === 'sealed' ? 'SEALED' : 'LOCKED';
98
+ violations.push({
99
+ type: 'file',
100
+ file: normalizedFile,
101
+ message: `File '${normalizedFile}' is ${label}.`,
102
+ });
103
+ continue; // No need to check functions if the whole file is locked
104
+ }
105
+
106
+ // ── Tier 2: Function-level lock ─────────────────────────────────────────
107
+ if (!entry || !entry.functions) continue;
108
+
109
+ const lockedFunctions = Object.entries(entry.functions)
110
+ .filter(([, fnData]) => fnData.status === 'locked')
111
+ .map(([name]) => name);
112
+
113
+ if (lockedFunctions.length === 0) continue;
114
+
115
+ // Re-extract function boundaries from the current on-disk file
116
+ const currentFunctions = extractFunctions(normalizedFile);
117
+
118
+ for (const lockedFnName of lockedFunctions) {
119
+ const fn = currentFunctions.find(f => f.name === lockedFnName);
120
+ if (!fn) {
121
+ // Function was deleted or renamed — this itself is a violation
122
+ violations.push({
123
+ type: 'function-missing',
124
+ file: normalizedFile,
125
+ fn: lockedFnName,
126
+ message: `Locked function '${lockedFnName}' in '${normalizedFile}' was removed or renamed.`,
127
+ });
128
+ continue;
129
+ }
130
+
131
+ // Check if any changed line falls within the function's boundaries
132
+ for (const line of changedLines.keys()) {
133
+ if (line >= fn.startLine && line <= fn.endLine) {
134
+ violations.push({
135
+ type: 'function',
136
+ file: normalizedFile,
137
+ fn: lockedFnName,
138
+ message:
139
+ `Locked function '${lockedFnName}' in '${normalizedFile}' was modified ` +
140
+ `(changed line ${line} is inside [${fn.startLine}–${fn.endLine}]).`,
141
+ });
142
+ break; // One violation per function is enough
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ // ── Report ────────────────────────────────────────────────────────────────
149
+ if (violations.length === 0) {
150
+ console.log('āœ… Scope guard passed — no locked files or functions were modified.');
151
+ return;
152
+ }
153
+
154
+ console.error(`\nāŒ Scope violations detected:\n`);
155
+ for (const v of violations) {
156
+ console.error(` VIOLATION: ${v.message}`);
157
+ }
158
+ console.error(
159
+ `\n${violations.length} violation(s) found.\n` +
160
+ ` • Revert unintentional changes with: git restore <file>\n` +
161
+ ` • Explicitly unlock with: scopelock unlock <file>[:<function>] "<reason>"`
162
+ );
163
+
164
+ process.exit(1);
165
+ }
166
+
167
+ module.exports = { guard };
package/src/manifest.js CHANGED
@@ -177,6 +177,63 @@ function lock(target, reason = 'manually locked') {
177
177
  }
178
178
  }
179
179
 
180
+ /**
181
+ * Seal a file — permanent, override-resistant production path lock.
182
+ * Cannot be removed by 'unlock'. Requires 'unseal' with a human-approved ticket.
183
+ *
184
+ * @param {string} file File path
185
+ * @param {string} reason Mandatory reason string
186
+ */
187
+ function seal(file, reason) {
188
+ const relativePath = file.replace(/\\/g, '/');
189
+ const manifest = getManifest();
190
+ ensureFileEntry(manifest, relativePath);
191
+ const entry = manifest.files[relativePath];
192
+
193
+ entry.status = 'sealed';
194
+ entry.history.push({
195
+ timestamp: new Date().toISOString(),
196
+ action: 'sealed',
197
+ reason,
198
+ });
199
+ saveManifest(manifest);
200
+ console.log(`šŸ” SEALED ${relativePath}. Only 'scopelock unseal' with a human-approved ticket can release this.`);
201
+ }
202
+
203
+ /**
204
+ * Remove a seal from a file. Requires an explicit human-approved ticket string.
205
+ * This is the only command that can override a 'sealed' file.
206
+ *
207
+ * @param {string} file File path
208
+ * @param {string} ticket Human-approved ticket (e.g. "JIRA-123" or "PR-456")
209
+ * @param {string} reason Mandatory reason string
210
+ */
211
+ function unseal(file, ticket, reason) {
212
+ if (!ticket || !reason) {
213
+ console.error('Usage: scopelock unseal <file> --human-approved=<ticket> <reason>');
214
+ process.exit(1);
215
+ }
216
+ const relativePath = file.replace(/\\/g, '/');
217
+ const manifest = getManifest();
218
+ ensureFileEntry(manifest, relativePath);
219
+ const entry = manifest.files[relativePath];
220
+
221
+ if (entry.status !== 'sealed') {
222
+ console.error(`'${relativePath}' is not sealed. Use 'scopelock unlock' instead.`);
223
+ process.exit(1);
224
+ }
225
+
226
+ entry.status = 'active';
227
+ entry.history.push({
228
+ timestamp: new Date().toISOString(),
229
+ action: 'unsealed',
230
+ humanApproved: ticket,
231
+ reason,
232
+ });
233
+ saveManifest(manifest);
234
+ console.log(`šŸ”“ UNSEALED ${relativePath}. Ticket: ${ticket}. Reason: ${reason}`);
235
+ }
236
+
180
237
  /**
181
238
  * Unlock a file or a specific function.
182
239
  *
@@ -206,6 +263,15 @@ function unlock(target, reason) {
206
263
  saveManifest(manifest);
207
264
  console.log(`šŸ”“ Unlocked function '${funcName}' in ${relativePath}. Reason: ${reason}`);
208
265
  } else {
266
+ // Guard against bypassing a seal with a normal unlock
267
+ if (entry.status === 'sealed') {
268
+ console.error(
269
+ `āŒ '${relativePath}' is SEALED and cannot be unlocked with 'scopelock unlock'.\n` +
270
+ ` This path is a protected production route.\n` +
271
+ ` Use: scopelock unseal ${relativePath} --human-approved=<ticket> <reason>`
272
+ );
273
+ process.exit(1);
274
+ }
209
275
  entry.status = 'active';
210
276
  entry.history.push(historyEntry);
211
277
  saveManifest(manifest);
@@ -220,9 +286,10 @@ function status() {
220
286
  const manifest = getManifest();
221
287
  const files = Object.entries(manifest.files);
222
288
 
223
- const locked = files.filter(([, v]) => v.status === 'locked');
224
- const active = files.filter(([, v]) => v.status === 'active');
225
- const unscoped = files.filter(([, v]) => v.status === 'unscoped');
289
+ const sealed = files.filter(([, v]) => v.status === 'sealed');
290
+ const locked = files.filter(([, v]) => v.status === 'locked');
291
+ const active = files.filter(([, v]) => v.status === 'active');
292
+ const unscoped = files.filter(([, v]) => v.status === 'unscoped');
226
293
 
227
294
  // Count locked functions across all files
228
295
  let lockedFnCount = 0;
@@ -232,10 +299,16 @@ function status() {
232
299
  }
233
300
 
234
301
  console.log(`\nšŸ“‹ scopelock status\n`);
302
+ console.log(` šŸ›”ļø sealed — ${sealed.length} file(s)`);
235
303
  console.log(` šŸ”’ locked — ${locked.length} file(s), ${lockedFnCount} function(s)`);
236
304
  console.log(` āœļø active — ${active.length} file(s)`);
237
305
  console.log(` ⬜ unscoped — ${unscoped.length} file(s)\n`);
238
306
 
307
+ if (sealed.length > 0) {
308
+ console.log('\nSealed files:');
309
+ sealed.forEach(([f]) => console.log(` ${f}`));
310
+ }
311
+
239
312
  if (locked.length > 0) {
240
313
  console.log(`Locked files:`);
241
314
  for (const [filePath, data] of locked) {
@@ -268,30 +341,20 @@ function status() {
268
341
  }
269
342
 
270
343
  /**
271
- * Allow a secret to be committed in a specific file.
344
+ * Trust a file to contain a mock secret, bypassing the Secret Sentinel.
272
345
  *
273
- * @param {string} file "<file>"
274
- * @param {string} reason Mandatory reason string.
346
+ * @param {string} file
347
+ * @param {string} reason
275
348
  */
276
- function allowSecret(file, reason) {
349
+ function trust(file, reason) {
277
350
  const relativePath = file.replace(/\\/g, '/');
278
351
  const manifest = getManifest();
279
-
280
- if (!manifest.allowedSecrets) {
281
- manifest.allowedSecrets = {};
282
- }
283
-
284
- if (!manifest.allowedSecrets[relativePath]) {
285
- manifest.allowedSecrets[relativePath] = [];
286
- }
287
-
288
- manifest.allowedSecrets[relativePath].push({
352
+ manifest.allowedSecrets[relativePath] = {
289
353
  timestamp: new Date().toISOString(),
290
- reason
291
- });
292
-
354
+ reason,
355
+ };
293
356
  saveManifest(manifest);
294
357
  console.log(`āš ļø Secret Sentinel bypassed for ${relativePath}. Reason: ${reason}`);
295
358
  }
296
359
 
297
- module.exports = { init, lock, unlock, allowSecret, status, getManifest, saveManifest };
360
+ module.exports = { init, lock, unlock, seal, unseal, trust, status, getManifest, saveManifest };