scene-capability-engine 3.6.2 → 3.6.4

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.
@@ -0,0 +1,632 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const { getSceStateStore } = require('../state/sce-state-store');
5
+
6
+ const DEFAULT_WRITE_AUTH_POLICY_PATH = '.sce/config/authorization-policy.json';
7
+ const DEFAULT_WRITE_AUTH_POLICY = Object.freeze({
8
+ enabled: false,
9
+ enforce_actions: ['studio:apply', 'studio:release', 'studio:rollback', 'task:rerun'],
10
+ default_ttl_minutes: 15,
11
+ max_ttl_minutes: 120,
12
+ require_password_for_grant: true,
13
+ require_password_for_revoke: false,
14
+ password_env: 'SCE_AUTH_PASSWORD',
15
+ default_scope: ['project:*'],
16
+ allow_test_bypass: true,
17
+ allow_password_as_inline_lease: false
18
+ });
19
+
20
+ function normalizeString(value) {
21
+ if (typeof value !== 'string') {
22
+ return '';
23
+ }
24
+ return value.trim();
25
+ }
26
+
27
+ function normalizeBoolean(value, fallback = false) {
28
+ if (typeof value === 'boolean') {
29
+ return value;
30
+ }
31
+
32
+ const normalized = normalizeString(`${value}`).toLowerCase();
33
+ if (!normalized) {
34
+ return fallback;
35
+ }
36
+ if (['1', 'true', 'yes', 'on', 'enabled'].includes(normalized)) {
37
+ return true;
38
+ }
39
+ if (['0', 'false', 'no', 'off', 'disabled'].includes(normalized)) {
40
+ return false;
41
+ }
42
+ return fallback;
43
+ }
44
+
45
+ function normalizeInteger(value, fallback = 0) {
46
+ const parsed = Number.parseInt(`${value}`, 10);
47
+ if (!Number.isFinite(parsed) || parsed <= 0) {
48
+ return fallback;
49
+ }
50
+ return parsed;
51
+ }
52
+
53
+ function normalizeStringArray(value, fallback = []) {
54
+ if (Array.isArray(value)) {
55
+ const normalized = value
56
+ .map((item) => normalizeString(item))
57
+ .filter(Boolean);
58
+ return normalized.length > 0 ? normalized : [...fallback];
59
+ }
60
+
61
+ const text = normalizeString(value);
62
+ if (!text) {
63
+ return [...fallback];
64
+ }
65
+ const split = text
66
+ .split(/[,\s]+/g)
67
+ .map((item) => normalizeString(item))
68
+ .filter(Boolean);
69
+ return split.length > 0 ? split : [...fallback];
70
+ }
71
+
72
+ function normalizeScopeList(value, fallback = ['project:*']) {
73
+ const normalized = normalizeStringArray(value, fallback)
74
+ .map((item) => item.toLowerCase())
75
+ .map((item) => item.replace(/\s+/g, ''))
76
+ .filter(Boolean);
77
+ return normalized.length > 0 ? Array.from(new Set(normalized)) : [...fallback];
78
+ }
79
+
80
+ function nowIso() {
81
+ return new Date().toISOString();
82
+ }
83
+
84
+ function resolveActor(env = process.env) {
85
+ const preferred = normalizeString(env.SCE_AUTH_ACTOR)
86
+ || normalizeString(env.SCE_AUTH_SUBJECT)
87
+ || normalizeString(env.USERNAME)
88
+ || normalizeString(env.USER);
89
+ if (preferred) {
90
+ return preferred;
91
+ }
92
+
93
+ try {
94
+ return normalizeString(os.userInfo().username) || 'unknown';
95
+ } catch (_error) {
96
+ return 'unknown';
97
+ }
98
+ }
99
+
100
+ function scopeMatchesAction(scope, action) {
101
+ const normalizedScope = normalizeString(scope).toLowerCase();
102
+ const normalizedAction = normalizeString(action).toLowerCase();
103
+ if (!normalizedScope || !normalizedAction) {
104
+ return false;
105
+ }
106
+ if (normalizedScope === '*' || normalizedScope === 'project:*') {
107
+ return true;
108
+ }
109
+ if (normalizedScope === normalizedAction) {
110
+ return true;
111
+ }
112
+ if (normalizedScope.endsWith(':*')) {
113
+ const prefix = normalizedScope.slice(0, -1);
114
+ return normalizedAction.startsWith(prefix);
115
+ }
116
+ if (normalizedScope.endsWith('*')) {
117
+ const prefix = normalizedScope.slice(0, -1);
118
+ return normalizedAction.startsWith(prefix);
119
+ }
120
+ return false;
121
+ }
122
+
123
+ function hasScopeForAction(scopeList = [], action = '') {
124
+ const normalizedScopes = normalizeScopeList(scopeList, []);
125
+ if (normalizedScopes.length === 0) {
126
+ return false;
127
+ }
128
+ return normalizedScopes.some((scope) => scopeMatchesAction(scope, action));
129
+ }
130
+
131
+ function normalizeWriteAuthPolicy(rawPolicy = {}) {
132
+ const enabled = normalizeBoolean(rawPolicy.enabled, DEFAULT_WRITE_AUTH_POLICY.enabled);
133
+ const enforceActions = normalizeScopeList(
134
+ rawPolicy.enforce_actions,
135
+ DEFAULT_WRITE_AUTH_POLICY.enforce_actions
136
+ );
137
+ const defaultScope = normalizeScopeList(
138
+ rawPolicy.default_scope,
139
+ DEFAULT_WRITE_AUTH_POLICY.default_scope
140
+ );
141
+ const defaultTtlMinutes = normalizeInteger(
142
+ rawPolicy.default_ttl_minutes,
143
+ DEFAULT_WRITE_AUTH_POLICY.default_ttl_minutes
144
+ );
145
+ const maxTtlMinutes = Math.max(
146
+ normalizeInteger(rawPolicy.max_ttl_minutes, DEFAULT_WRITE_AUTH_POLICY.max_ttl_minutes),
147
+ defaultTtlMinutes
148
+ );
149
+
150
+ return {
151
+ enabled,
152
+ enforce_actions: enforceActions,
153
+ default_ttl_minutes: defaultTtlMinutes,
154
+ max_ttl_minutes: maxTtlMinutes,
155
+ require_password_for_grant: normalizeBoolean(
156
+ rawPolicy.require_password_for_grant,
157
+ DEFAULT_WRITE_AUTH_POLICY.require_password_for_grant
158
+ ),
159
+ require_password_for_revoke: normalizeBoolean(
160
+ rawPolicy.require_password_for_revoke,
161
+ DEFAULT_WRITE_AUTH_POLICY.require_password_for_revoke
162
+ ),
163
+ password_env: normalizeString(rawPolicy.password_env) || DEFAULT_WRITE_AUTH_POLICY.password_env,
164
+ default_scope: defaultScope,
165
+ allow_test_bypass: normalizeBoolean(
166
+ rawPolicy.allow_test_bypass,
167
+ DEFAULT_WRITE_AUTH_POLICY.allow_test_bypass
168
+ ),
169
+ allow_password_as_inline_lease: normalizeBoolean(
170
+ rawPolicy.allow_password_as_inline_lease,
171
+ DEFAULT_WRITE_AUTH_POLICY.allow_password_as_inline_lease
172
+ )
173
+ };
174
+ }
175
+
176
+ function applyPolicyEnvOverrides(policy = {}, env = process.env) {
177
+ const overrideEnabled = normalizeString(env.SCE_AUTH_REQUIRE_LEASE);
178
+ const overridePasswordEnv = normalizeString(env.SCE_AUTH_PASSWORD_ENV);
179
+ const overrideEnforceActions = normalizeString(env.SCE_AUTH_ENFORCE_ACTIONS);
180
+ const overrideDefaultTtl = normalizeString(env.SCE_AUTH_DEFAULT_TTL_MINUTES);
181
+ const overrideMaxTtl = normalizeString(env.SCE_AUTH_MAX_TTL_MINUTES);
182
+
183
+ return normalizeWriteAuthPolicy({
184
+ ...policy,
185
+ enabled: overrideEnabled ? normalizeBoolean(overrideEnabled, policy.enabled) : policy.enabled,
186
+ password_env: overridePasswordEnv || policy.password_env,
187
+ enforce_actions: overrideEnforceActions
188
+ ? normalizeScopeList(overrideEnforceActions, policy.enforce_actions)
189
+ : policy.enforce_actions,
190
+ default_ttl_minutes: overrideDefaultTtl
191
+ ? normalizeInteger(overrideDefaultTtl, policy.default_ttl_minutes)
192
+ : policy.default_ttl_minutes,
193
+ max_ttl_minutes: overrideMaxTtl
194
+ ? normalizeInteger(overrideMaxTtl, policy.max_ttl_minutes)
195
+ : policy.max_ttl_minutes,
196
+ require_password_for_grant: normalizeString(env.SCE_AUTH_REQUIRE_PASSWORD_FOR_GRANT)
197
+ ? normalizeBoolean(env.SCE_AUTH_REQUIRE_PASSWORD_FOR_GRANT, policy.require_password_for_grant)
198
+ : policy.require_password_for_grant,
199
+ require_password_for_revoke: normalizeString(env.SCE_AUTH_REQUIRE_PASSWORD_FOR_REVOKE)
200
+ ? normalizeBoolean(env.SCE_AUTH_REQUIRE_PASSWORD_FOR_REVOKE, policy.require_password_for_revoke)
201
+ : policy.require_password_for_revoke,
202
+ allow_test_bypass: normalizeString(env.SCE_AUTH_ALLOW_TEST_BYPASS)
203
+ ? normalizeBoolean(env.SCE_AUTH_ALLOW_TEST_BYPASS, policy.allow_test_bypass)
204
+ : policy.allow_test_bypass,
205
+ allow_password_as_inline_lease: normalizeString(env.SCE_AUTH_INLINE_PASSWORD_LEASE)
206
+ ? normalizeBoolean(env.SCE_AUTH_INLINE_PASSWORD_LEASE, policy.allow_password_as_inline_lease)
207
+ : policy.allow_password_as_inline_lease
208
+ });
209
+ }
210
+
211
+ function sanitizePolicyForOutput(policy = {}) {
212
+ return {
213
+ enabled: policy.enabled === true,
214
+ enforce_actions: normalizeScopeList(policy.enforce_actions, DEFAULT_WRITE_AUTH_POLICY.enforce_actions),
215
+ default_ttl_minutes: normalizeInteger(policy.default_ttl_minutes, DEFAULT_WRITE_AUTH_POLICY.default_ttl_minutes),
216
+ max_ttl_minutes: normalizeInteger(policy.max_ttl_minutes, DEFAULT_WRITE_AUTH_POLICY.max_ttl_minutes),
217
+ require_password_for_grant: policy.require_password_for_grant === true,
218
+ require_password_for_revoke: policy.require_password_for_revoke === true,
219
+ password_env: normalizeString(policy.password_env) || DEFAULT_WRITE_AUTH_POLICY.password_env,
220
+ default_scope: normalizeScopeList(policy.default_scope, DEFAULT_WRITE_AUTH_POLICY.default_scope),
221
+ allow_test_bypass: policy.allow_test_bypass === true,
222
+ allow_password_as_inline_lease: policy.allow_password_as_inline_lease === true
223
+ };
224
+ }
225
+
226
+ async function loadWriteAuthorizationPolicy(projectPath = process.cwd(), fileSystem = fs, env = process.env) {
227
+ const policyPath = path.join(projectPath, DEFAULT_WRITE_AUTH_POLICY_PATH);
228
+ let filePolicy = {};
229
+
230
+ if (await fileSystem.pathExists(policyPath)) {
231
+ try {
232
+ filePolicy = await fileSystem.readJson(policyPath);
233
+ } catch (error) {
234
+ throw new Error(`Failed to read write authorization policy: ${error.message}`);
235
+ }
236
+ }
237
+
238
+ const merged = normalizeWriteAuthPolicy({
239
+ ...DEFAULT_WRITE_AUTH_POLICY,
240
+ ...(filePolicy || {})
241
+ });
242
+ const normalized = applyPolicyEnvOverrides(merged, env);
243
+
244
+ return {
245
+ policy: normalized,
246
+ policy_path: DEFAULT_WRITE_AUTH_POLICY_PATH
247
+ };
248
+ }
249
+
250
+ function shouldEnforceAction(policy = {}, action = '', options = {}) {
251
+ if (options.requireAuth === true) {
252
+ return true;
253
+ }
254
+ if (policy.enabled !== true) {
255
+ return false;
256
+ }
257
+ return hasScopeForAction(policy.enforce_actions || [], action);
258
+ }
259
+
260
+ function resolveStore(projectPath, dependencies = {}) {
261
+ return getSceStateStore(projectPath, {
262
+ fileSystem: dependencies.fileSystem || fs,
263
+ env: dependencies.env || process.env,
264
+ sqliteModule: dependencies.sqliteModule
265
+ });
266
+ }
267
+
268
+ function ensureLeaseActive(lease = {}, now = nowIso()) {
269
+ if (!lease || typeof lease !== 'object') {
270
+ return { ok: false, reason: 'lease_not_found' };
271
+ }
272
+ if (normalizeString(lease.revoked_at)) {
273
+ return { ok: false, reason: 'lease_revoked' };
274
+ }
275
+ const expiresAt = Date.parse(normalizeString(lease.expires_at));
276
+ const nowTs = Date.parse(normalizeString(now));
277
+ if (!Number.isFinite(expiresAt) || !Number.isFinite(nowTs) || expiresAt <= nowTs) {
278
+ return { ok: false, reason: 'lease_expired' };
279
+ }
280
+ return { ok: true, reason: '' };
281
+ }
282
+
283
+ async function appendAuthAuditEvent(stateStore, payload = {}) {
284
+ const appended = await stateStore.appendAuthEvent(payload);
285
+ if (!appended) {
286
+ throw new Error('Failed to persist authorization audit event into sqlite state store');
287
+ }
288
+ }
289
+
290
+ function ensurePolicyPassword(options = {}, dependencies = {}, policy = {}, actionLabel = 'grant') {
291
+ const env = dependencies.env || process.env;
292
+ const passwordEnv = normalizeString(policy.password_env) || DEFAULT_WRITE_AUTH_POLICY.password_env;
293
+ const expected = normalizeString(dependencies.authSecret || env[passwordEnv]);
294
+ if (!expected) {
295
+ throw new Error(
296
+ `Authorization policy requires password for ${actionLabel}, but ${passwordEnv} is not configured`
297
+ );
298
+ }
299
+
300
+ const provided = normalizeString(options.authPassword);
301
+ if (!provided) {
302
+ throw new Error(`Authorization password required for ${actionLabel}. Provide --auth-password`);
303
+ }
304
+ if (provided !== expected) {
305
+ throw new Error(`Authorization password check failed for ${actionLabel}`);
306
+ }
307
+
308
+ return passwordEnv;
309
+ }
310
+
311
+ function normalizeTtlMinutes(ttlValue, policy = {}) {
312
+ const defaultTtl = normalizeInteger(policy.default_ttl_minutes, DEFAULT_WRITE_AUTH_POLICY.default_ttl_minutes);
313
+ const maxTtl = normalizeInteger(policy.max_ttl_minutes, DEFAULT_WRITE_AUTH_POLICY.max_ttl_minutes);
314
+ const requested = normalizeInteger(ttlValue, defaultTtl);
315
+ return Math.min(Math.max(requested, 1), Math.max(maxTtl, 1));
316
+ }
317
+
318
+ async function grantWriteAuthorizationLease(options = {}, dependencies = {}) {
319
+ const projectPath = dependencies.projectPath || process.cwd();
320
+ const fileSystem = dependencies.fileSystem || fs;
321
+ const env = dependencies.env || process.env;
322
+ const loadedPolicy = await loadWriteAuthorizationPolicy(projectPath, fileSystem, env);
323
+ const policy = loadedPolicy.policy;
324
+
325
+ let passwordEnv = null;
326
+ if (policy.require_password_for_grant) {
327
+ passwordEnv = ensurePolicyPassword(options, dependencies, policy, 'auth grant');
328
+ }
329
+
330
+ const ttlMinutes = normalizeTtlMinutes(options.ttlMinutes || options.ttl_minutes, policy);
331
+ const actor = normalizeString(options.actor) || resolveActor(env);
332
+ const subject = normalizeString(options.subject) || actor;
333
+ const role = normalizeString(options.role) || 'maintainer';
334
+ const scope = normalizeScopeList(options.scope, policy.default_scope);
335
+ const reason = normalizeString(options.reason) || 'manual-auth-grant';
336
+ const metadata = options.metadata && typeof options.metadata === 'object'
337
+ ? { ...options.metadata }
338
+ : {};
339
+
340
+ const stateStore = resolveStore(projectPath, { ...dependencies, fileSystem, env });
341
+ const lease = await stateStore.issueAuthLease({
342
+ subject,
343
+ role,
344
+ scope,
345
+ reason,
346
+ metadata: {
347
+ ...metadata,
348
+ actor,
349
+ source: metadata.source || 'sce auth grant'
350
+ },
351
+ ttl_minutes: ttlMinutes
352
+ });
353
+ if (!lease) {
354
+ throw new Error('SQLite state backend unavailable while issuing auth lease');
355
+ }
356
+
357
+ await appendAuthAuditEvent(stateStore, {
358
+ event_type: 'lease.granted',
359
+ action: 'auth:grant',
360
+ actor,
361
+ lease_id: lease.lease_id,
362
+ result: 'allow',
363
+ target: subject,
364
+ detail: {
365
+ role,
366
+ scope,
367
+ reason,
368
+ ttl_minutes: ttlMinutes
369
+ }
370
+ });
371
+
372
+ return {
373
+ success: true,
374
+ policy: sanitizePolicyForOutput(policy),
375
+ policy_path: loadedPolicy.policy_path,
376
+ password_env: passwordEnv,
377
+ lease,
378
+ store_path: stateStore.getStoreRelativePath()
379
+ };
380
+ }
381
+
382
+ async function revokeWriteAuthorizationLease(leaseId, options = {}, dependencies = {}) {
383
+ const normalizedLeaseId = normalizeString(leaseId);
384
+ if (!normalizedLeaseId) {
385
+ throw new Error('lease id is required');
386
+ }
387
+
388
+ const projectPath = dependencies.projectPath || process.cwd();
389
+ const fileSystem = dependencies.fileSystem || fs;
390
+ const env = dependencies.env || process.env;
391
+ const loadedPolicy = await loadWriteAuthorizationPolicy(projectPath, fileSystem, env);
392
+ const policy = loadedPolicy.policy;
393
+
394
+ if (policy.require_password_for_revoke) {
395
+ ensurePolicyPassword(options, dependencies, policy, 'auth revoke');
396
+ }
397
+
398
+ const actor = normalizeString(options.actor) || resolveActor(env);
399
+ const reason = normalizeString(options.reason) || 'manual-auth-revoke';
400
+ const stateStore = resolveStore(projectPath, { ...dependencies, fileSystem, env });
401
+
402
+ const lease = await stateStore.revokeAuthLease(normalizedLeaseId);
403
+ if (!lease) {
404
+ throw new Error(`Auth lease not found: ${normalizedLeaseId}`);
405
+ }
406
+
407
+ await appendAuthAuditEvent(stateStore, {
408
+ event_type: 'lease.revoked',
409
+ action: 'auth:revoke',
410
+ actor,
411
+ lease_id: normalizedLeaseId,
412
+ result: 'allow',
413
+ target: lease.subject || null,
414
+ detail: {
415
+ reason
416
+ }
417
+ });
418
+
419
+ return {
420
+ success: true,
421
+ policy: sanitizePolicyForOutput(policy),
422
+ policy_path: loadedPolicy.policy_path,
423
+ lease,
424
+ store_path: stateStore.getStoreRelativePath()
425
+ };
426
+ }
427
+
428
+ async function ensureWriteAuthorization(action, options = {}, dependencies = {}) {
429
+ const normalizedAction = normalizeString(action).toLowerCase();
430
+ if (!normalizedAction) {
431
+ throw new Error('write authorization action is required');
432
+ }
433
+
434
+ const projectPath = dependencies.projectPath || process.cwd();
435
+ const fileSystem = dependencies.fileSystem || fs;
436
+ const env = dependencies.env || process.env;
437
+ const loadedPolicy = await loadWriteAuthorizationPolicy(projectPath, fileSystem, env);
438
+ const policy = loadedPolicy.policy;
439
+ const enforce = shouldEnforceAction(policy, normalizedAction, options);
440
+
441
+ if (!enforce) {
442
+ return {
443
+ required: false,
444
+ passed: true,
445
+ action: normalizedAction,
446
+ policy: sanitizePolicyForOutput(policy),
447
+ policy_path: loadedPolicy.policy_path
448
+ };
449
+ }
450
+
451
+ if (policy.allow_test_bypass && normalizeString(env.NODE_ENV).toLowerCase() === 'test') {
452
+ return {
453
+ required: true,
454
+ passed: true,
455
+ bypassed: 'test-env',
456
+ action: normalizedAction,
457
+ policy: sanitizePolicyForOutput(policy),
458
+ policy_path: loadedPolicy.policy_path
459
+ };
460
+ }
461
+
462
+ const actor = normalizeString(options.actor) || resolveActor(env);
463
+ const stateStore = resolveStore(projectPath, { ...dependencies, fileSystem, env });
464
+ let leaseId = normalizeString(options.authLease || options.authLeaseId);
465
+
466
+ if (!leaseId && policy.allow_password_as_inline_lease && normalizeString(options.authPassword)) {
467
+ const granted = await grantWriteAuthorizationLease({
468
+ subject: actor,
469
+ role: 'maintainer',
470
+ scope: [normalizedAction],
471
+ reason: `inline-auth:${normalizedAction}`,
472
+ authPassword: options.authPassword,
473
+ metadata: {
474
+ source: `inline:${normalizedAction}`
475
+ }
476
+ }, {
477
+ projectPath,
478
+ fileSystem,
479
+ env,
480
+ authSecret: dependencies.authSecret
481
+ });
482
+ leaseId = normalizeString(granted?.lease?.lease_id);
483
+ }
484
+
485
+ if (!leaseId) {
486
+ await appendAuthAuditEvent(stateStore, {
487
+ event_type: 'authorization.denied',
488
+ action: normalizedAction,
489
+ actor,
490
+ result: 'deny',
491
+ detail: {
492
+ reason: 'lease_required'
493
+ }
494
+ });
495
+ throw new Error(
496
+ `Write authorization required for ${normalizedAction}. Run: sce auth grant --scope ${normalizedAction} --reason "<reason>"`
497
+ );
498
+ }
499
+
500
+ const lease = await stateStore.getAuthLease(leaseId);
501
+ if (!lease) {
502
+ await appendAuthAuditEvent(stateStore, {
503
+ event_type: 'authorization.denied',
504
+ action: normalizedAction,
505
+ actor,
506
+ lease_id: leaseId,
507
+ result: 'deny',
508
+ detail: {
509
+ reason: 'lease_not_found'
510
+ }
511
+ });
512
+ throw new Error(`Write authorization denied for ${normalizedAction}: lease not found (${leaseId})`);
513
+ }
514
+
515
+ const active = ensureLeaseActive(lease, nowIso());
516
+ if (!active.ok) {
517
+ await appendAuthAuditEvent(stateStore, {
518
+ event_type: 'authorization.denied',
519
+ action: normalizedAction,
520
+ actor,
521
+ lease_id: leaseId,
522
+ result: 'deny',
523
+ target: lease.subject || null,
524
+ detail: {
525
+ reason: active.reason
526
+ }
527
+ });
528
+ throw new Error(`Write authorization denied for ${normalizedAction}: ${active.reason}`);
529
+ }
530
+
531
+ if (!hasScopeForAction(lease.scope || [], normalizedAction)) {
532
+ await appendAuthAuditEvent(stateStore, {
533
+ event_type: 'authorization.denied',
534
+ action: normalizedAction,
535
+ actor,
536
+ lease_id: leaseId,
537
+ result: 'deny',
538
+ target: lease.subject || null,
539
+ detail: {
540
+ reason: 'scope_mismatch',
541
+ scope: lease.scope || []
542
+ }
543
+ });
544
+ throw new Error(`Write authorization denied for ${normalizedAction}: scope mismatch`);
545
+ }
546
+
547
+ await appendAuthAuditEvent(stateStore, {
548
+ event_type: 'authorization.allowed',
549
+ action: normalizedAction,
550
+ actor,
551
+ lease_id: leaseId,
552
+ result: 'allow',
553
+ target: lease.subject || null,
554
+ detail: {
555
+ scope: lease.scope || [],
556
+ expires_at: lease.expires_at || null
557
+ }
558
+ });
559
+
560
+ return {
561
+ required: true,
562
+ passed: true,
563
+ action: normalizedAction,
564
+ lease_id: lease.lease_id,
565
+ lease_subject: lease.subject || null,
566
+ lease_role: lease.role || null,
567
+ lease_scope: Array.isArray(lease.scope) ? [...lease.scope] : [],
568
+ lease_expires_at: lease.expires_at || null,
569
+ policy: sanitizePolicyForOutput(policy),
570
+ policy_path: loadedPolicy.policy_path,
571
+ store_path: stateStore.getStoreRelativePath()
572
+ };
573
+ }
574
+
575
+ async function getWriteAuthorizationLease(leaseId, dependencies = {}) {
576
+ const normalizedLeaseId = normalizeString(leaseId);
577
+ if (!normalizedLeaseId) {
578
+ return null;
579
+ }
580
+
581
+ const projectPath = dependencies.projectPath || process.cwd();
582
+ const fileSystem = dependencies.fileSystem || fs;
583
+ const env = dependencies.env || process.env;
584
+ const stateStore = resolveStore(projectPath, { ...dependencies, fileSystem, env });
585
+ return stateStore.getAuthLease(normalizedLeaseId);
586
+ }
587
+
588
+ async function collectWriteAuthorizationStatus(options = {}, dependencies = {}) {
589
+ const projectPath = dependencies.projectPath || process.cwd();
590
+ const fileSystem = dependencies.fileSystem || fs;
591
+ const env = dependencies.env || process.env;
592
+ const loadedPolicy = await loadWriteAuthorizationPolicy(projectPath, fileSystem, env);
593
+ const stateStore = resolveStore(projectPath, { ...dependencies, fileSystem, env });
594
+
595
+ const activeOnly = options.activeOnly !== false;
596
+ const limit = normalizeInteger(options.limit, 20);
597
+ const eventsLimit = normalizeInteger(options.eventsLimit, 20);
598
+
599
+ const leases = await stateStore.listAuthLeases({
600
+ activeOnly,
601
+ limit
602
+ });
603
+ const events = await stateStore.listAuthEvents({
604
+ limit: eventsLimit
605
+ });
606
+
607
+ if (leases === null || events === null) {
608
+ throw new Error('SQLite state backend unavailable while reading authorization status');
609
+ }
610
+
611
+ return {
612
+ success: true,
613
+ policy: sanitizePolicyForOutput(loadedPolicy.policy),
614
+ policy_path: loadedPolicy.policy_path,
615
+ leases,
616
+ events,
617
+ store_path: stateStore.getStoreRelativePath()
618
+ };
619
+ }
620
+
621
+ module.exports = {
622
+ DEFAULT_WRITE_AUTH_POLICY,
623
+ DEFAULT_WRITE_AUTH_POLICY_PATH,
624
+ normalizeWriteAuthPolicy,
625
+ loadWriteAuthorizationPolicy,
626
+ grantWriteAuthorizationLease,
627
+ revokeWriteAuthorizationLease,
628
+ ensureWriteAuthorization,
629
+ collectWriteAuthorizationStatus,
630
+ getWriteAuthorizationLease,
631
+ hasScopeForAction
632
+ };