scene-capability-engine 3.6.2 → 3.6.3

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.
@@ -1,4 +1,5 @@
1
- const path = require('path');
1
+ const path = require('path');
2
+ const crypto = require('crypto');
2
3
  const fs = require('fs-extra');
3
4
 
4
5
  const DEFAULT_BACKEND = 'sqlite';
@@ -31,6 +32,27 @@ function parseJsonSafe(value, fallback) {
31
32
  }
32
33
  }
33
34
 
35
+ function normalizeStringArray(value, fallback = []) {
36
+ if (!Array.isArray(value)) {
37
+ return [...fallback];
38
+ }
39
+ return value
40
+ .map((item) => normalizeString(item))
41
+ .filter(Boolean);
42
+ }
43
+
44
+ function normalizeIsoTimestamp(value, fallback = '') {
45
+ const normalized = normalizeString(value);
46
+ if (!normalized) {
47
+ return normalizeString(fallback);
48
+ }
49
+ const parsed = Date.parse(normalized);
50
+ if (!Number.isFinite(parsed)) {
51
+ return normalizeString(fallback);
52
+ }
53
+ return new Date(parsed).toISOString();
54
+ }
55
+
34
56
  function formatSegment(value) {
35
57
  const normalized = normalizeInteger(value, 0);
36
58
  if (normalized <= 0) {
@@ -83,6 +105,8 @@ class SceStateStore {
83
105
  specs: {},
84
106
  tasks: {},
85
107
  refs: {},
108
+ auth_leases: {},
109
+ auth_events: [],
86
110
  sequences: {
87
111
  scene_next: 1,
88
112
  spec_next_by_scene: {},
@@ -181,6 +205,39 @@ class SceStateStore {
181
205
 
182
206
  CREATE INDEX IF NOT EXISTS idx_studio_event_stream_job_ts
183
207
  ON studio_event_stream(job_id, event_timestamp);
208
+
209
+ CREATE TABLE IF NOT EXISTS auth_lease_registry (
210
+ lease_id TEXT PRIMARY KEY,
211
+ subject TEXT NOT NULL,
212
+ role TEXT NOT NULL,
213
+ scope_json TEXT NOT NULL,
214
+ reason TEXT,
215
+ metadata_json TEXT,
216
+ issued_at TEXT NOT NULL,
217
+ expires_at TEXT NOT NULL,
218
+ revoked_at TEXT,
219
+ created_at TEXT NOT NULL,
220
+ updated_at TEXT NOT NULL
221
+ );
222
+
223
+ CREATE INDEX IF NOT EXISTS idx_auth_lease_registry_expires
224
+ ON auth_lease_registry(expires_at);
225
+
226
+ CREATE TABLE IF NOT EXISTS auth_event_stream (
227
+ event_id TEXT PRIMARY KEY,
228
+ event_timestamp TEXT NOT NULL,
229
+ event_type TEXT NOT NULL,
230
+ action TEXT,
231
+ actor TEXT,
232
+ lease_id TEXT,
233
+ result TEXT,
234
+ target TEXT,
235
+ detail_json TEXT,
236
+ created_at TEXT NOT NULL
237
+ );
238
+
239
+ CREATE INDEX IF NOT EXISTS idx_auth_event_stream_ts
240
+ ON auth_event_stream(event_timestamp);
184
241
  `);
185
242
  }
186
243
 
@@ -264,6 +321,43 @@ class SceStateStore {
264
321
  };
265
322
  }
266
323
 
324
+ _mapAuthLeaseRow(row) {
325
+ if (!row) {
326
+ return null;
327
+ }
328
+ return {
329
+ lease_id: normalizeString(row.lease_id),
330
+ subject: normalizeString(row.subject),
331
+ role: normalizeString(row.role),
332
+ scope: normalizeStringArray(parseJsonSafe(row.scope_json, []), ['project:*']),
333
+ reason: normalizeString(row.reason) || null,
334
+ metadata: parseJsonSafe(row.metadata_json, {}) || {},
335
+ issued_at: normalizeIsoTimestamp(row.issued_at) || null,
336
+ expires_at: normalizeIsoTimestamp(row.expires_at) || null,
337
+ revoked_at: normalizeIsoTimestamp(row.revoked_at) || null,
338
+ created_at: normalizeIsoTimestamp(row.created_at) || null,
339
+ updated_at: normalizeIsoTimestamp(row.updated_at) || null
340
+ };
341
+ }
342
+
343
+ _mapAuthEventRow(row) {
344
+ if (!row) {
345
+ return null;
346
+ }
347
+ return {
348
+ event_id: normalizeString(row.event_id),
349
+ event_timestamp: normalizeIsoTimestamp(row.event_timestamp) || null,
350
+ event_type: normalizeString(row.event_type),
351
+ action: normalizeString(row.action) || null,
352
+ actor: normalizeString(row.actor) || null,
353
+ lease_id: normalizeString(row.lease_id) || null,
354
+ result: normalizeString(row.result) || null,
355
+ target: normalizeString(row.target) || null,
356
+ detail: parseJsonSafe(row.detail_json, {}) || {},
357
+ created_at: normalizeIsoTimestamp(row.created_at) || null
358
+ };
359
+ }
360
+
267
361
  async resolveOrCreateTaskRef(options = {}) {
268
362
  const sceneId = normalizeString(options.sceneId);
269
363
  const specId = normalizeString(options.specId);
@@ -510,6 +604,287 @@ class SceStateStore {
510
604
  return events;
511
605
  }
512
606
 
607
+ async issueAuthLease(options = {}) {
608
+ const subject = normalizeString(options.subject) || 'unknown';
609
+ const role = normalizeString(options.role) || 'maintainer';
610
+ const scope = normalizeStringArray(options.scope, ['project:*']);
611
+ const reason = normalizeString(options.reason) || null;
612
+ const metadata = options.metadata && typeof options.metadata === 'object'
613
+ ? options.metadata
614
+ : {};
615
+ const issuedAt = normalizeIsoTimestamp(options.issued_at || options.issuedAt, this.now()) || this.now();
616
+ const ttlMinutes = normalizeInteger(options.ttl_minutes || options.ttlMinutes, 15);
617
+ const fallbackExpiresAt = new Date(
618
+ (Date.parse(issuedAt) || Date.now()) + (Math.max(ttlMinutes, 1) * 60 * 1000)
619
+ ).toISOString();
620
+ const expiresAt = normalizeIsoTimestamp(options.expires_at || options.expiresAt, fallbackExpiresAt) || fallbackExpiresAt;
621
+ const leaseId = normalizeString(options.lease_id || options.leaseId)
622
+ || `lease-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
623
+ const nowIso = this.now();
624
+
625
+ if (this._useMemoryBackend()) {
626
+ return this._issueAuthLeaseInMemory({
627
+ leaseId,
628
+ subject,
629
+ role,
630
+ scope,
631
+ reason,
632
+ metadata,
633
+ issuedAt,
634
+ expiresAt,
635
+ nowIso
636
+ });
637
+ }
638
+
639
+ if (!await this.ensureReady()) {
640
+ return null;
641
+ }
642
+
643
+ this._db
644
+ .prepare(`
645
+ INSERT OR REPLACE INTO auth_lease_registry(
646
+ lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
647
+ )
648
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
649
+ `)
650
+ .run(
651
+ leaseId,
652
+ subject,
653
+ role,
654
+ JSON.stringify(scope),
655
+ reason,
656
+ JSON.stringify(metadata),
657
+ issuedAt,
658
+ expiresAt,
659
+ nowIso,
660
+ nowIso
661
+ );
662
+
663
+ return this.getAuthLease(leaseId);
664
+ }
665
+
666
+ async getAuthLease(leaseId) {
667
+ const normalizedLeaseId = normalizeString(leaseId);
668
+ if (!normalizedLeaseId) {
669
+ return null;
670
+ }
671
+
672
+ if (this._useMemoryBackend()) {
673
+ const row = this._memory.auth_leases[normalizedLeaseId];
674
+ return row
675
+ ? {
676
+ ...row,
677
+ scope: normalizeStringArray(row.scope, ['project:*']),
678
+ metadata: { ...(row.metadata || {}) }
679
+ }
680
+ : null;
681
+ }
682
+
683
+ if (!await this.ensureReady()) {
684
+ return null;
685
+ }
686
+
687
+ const row = this._db
688
+ .prepare(`
689
+ SELECT lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
690
+ FROM auth_lease_registry
691
+ WHERE lease_id = ?
692
+ `)
693
+ .get(normalizedLeaseId);
694
+ return this._mapAuthLeaseRow(row);
695
+ }
696
+
697
+ async listAuthLeases(options = {}) {
698
+ const activeOnly = options.activeOnly !== false;
699
+ const limit = normalizeInteger(options.limit, 20);
700
+ const nowIso = this.now();
701
+
702
+ if (this._useMemoryBackend()) {
703
+ let rows = Object.values(this._memory.auth_leases || {}).map((item) => ({
704
+ ...item,
705
+ scope: normalizeStringArray(item.scope, ['project:*']),
706
+ metadata: { ...(item.metadata || {}) }
707
+ }));
708
+ if (activeOnly) {
709
+ const nowTime = Date.parse(nowIso) || Date.now();
710
+ rows = rows.filter((item) => {
711
+ const revokedAt = normalizeString(item.revoked_at);
712
+ if (revokedAt) {
713
+ return false;
714
+ }
715
+ const expiresAt = Date.parse(item.expires_at || '') || 0;
716
+ return expiresAt > nowTime;
717
+ });
718
+ }
719
+ rows.sort((left, right) => (Date.parse(right.created_at || '') || 0) - (Date.parse(left.created_at || '') || 0));
720
+ return limit > 0 ? rows.slice(0, limit) : rows;
721
+ }
722
+
723
+ if (!await this.ensureReady()) {
724
+ return null;
725
+ }
726
+
727
+ const query = activeOnly
728
+ ? `
729
+ SELECT lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
730
+ FROM auth_lease_registry
731
+ WHERE revoked_at IS NULL AND expires_at > ?
732
+ ORDER BY created_at DESC
733
+ LIMIT ?
734
+ `
735
+ : `
736
+ SELECT lease_id, subject, role, scope_json, reason, metadata_json, issued_at, expires_at, revoked_at, created_at, updated_at
737
+ FROM auth_lease_registry
738
+ ORDER BY created_at DESC
739
+ LIMIT ?
740
+ `;
741
+
742
+ const statement = this._db.prepare(query);
743
+ const rows = activeOnly
744
+ ? statement.all(nowIso, limit)
745
+ : statement.all(limit);
746
+ return rows
747
+ .map((row) => this._mapAuthLeaseRow(row))
748
+ .filter(Boolean);
749
+ }
750
+
751
+ async revokeAuthLease(leaseId, options = {}) {
752
+ const normalizedLeaseId = normalizeString(leaseId);
753
+ if (!normalizedLeaseId) {
754
+ return null;
755
+ }
756
+ const revokedAt = normalizeIsoTimestamp(options.revoked_at || options.revokedAt, this.now()) || this.now();
757
+
758
+ if (this._useMemoryBackend()) {
759
+ const existing = this._memory.auth_leases[normalizedLeaseId];
760
+ if (!existing) {
761
+ return null;
762
+ }
763
+ existing.revoked_at = revokedAt;
764
+ existing.updated_at = revokedAt;
765
+ this._memory.auth_leases[normalizedLeaseId] = existing;
766
+ return {
767
+ ...existing,
768
+ scope: normalizeStringArray(existing.scope, ['project:*']),
769
+ metadata: { ...(existing.metadata || {}) }
770
+ };
771
+ }
772
+
773
+ if (!await this.ensureReady()) {
774
+ return null;
775
+ }
776
+
777
+ this._db
778
+ .prepare('UPDATE auth_lease_registry SET revoked_at = ?, updated_at = ? WHERE lease_id = ?')
779
+ .run(revokedAt, revokedAt, normalizedLeaseId);
780
+ return this.getAuthLease(normalizedLeaseId);
781
+ }
782
+
783
+ async appendAuthEvent(event = {}) {
784
+ const eventType = normalizeString(event.event_type || event.eventType);
785
+ if (!eventType) {
786
+ return false;
787
+ }
788
+ const eventId = normalizeString(event.event_id || event.eventId)
789
+ || `auth-evt-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
790
+ const timestamp = normalizeIsoTimestamp(event.event_timestamp || event.timestamp, this.now()) || this.now();
791
+ const normalizedEvent = {
792
+ event_id: eventId,
793
+ event_timestamp: timestamp,
794
+ event_type: eventType,
795
+ action: normalizeString(event.action) || null,
796
+ actor: normalizeString(event.actor) || null,
797
+ lease_id: normalizeString(event.lease_id || event.leaseId) || null,
798
+ result: normalizeString(event.result) || null,
799
+ target: normalizeString(event.target) || null,
800
+ detail: event.detail && typeof event.detail === 'object'
801
+ ? event.detail
802
+ : {}
803
+ };
804
+
805
+ if (this._useMemoryBackend()) {
806
+ const existingIndex = this._memory.auth_events
807
+ .findIndex((item) => normalizeString(item.event_id) === eventId);
808
+ const row = {
809
+ ...normalizedEvent,
810
+ created_at: this.now()
811
+ };
812
+ if (existingIndex >= 0) {
813
+ this._memory.auth_events[existingIndex] = row;
814
+ } else {
815
+ this._memory.auth_events.push(row);
816
+ }
817
+ this._memory.auth_events.sort((left, right) => {
818
+ const l = Date.parse(left.event_timestamp || '') || 0;
819
+ const r = Date.parse(right.event_timestamp || '') || 0;
820
+ return l - r;
821
+ });
822
+ return true;
823
+ }
824
+
825
+ if (!await this.ensureReady()) {
826
+ return false;
827
+ }
828
+
829
+ this._db
830
+ .prepare(`
831
+ INSERT OR REPLACE INTO auth_event_stream(
832
+ event_id, event_timestamp, event_type, action, actor, lease_id, result, target, detail_json, created_at
833
+ )
834
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
835
+ `)
836
+ .run(
837
+ normalizedEvent.event_id,
838
+ normalizedEvent.event_timestamp,
839
+ normalizedEvent.event_type,
840
+ normalizedEvent.action,
841
+ normalizedEvent.actor,
842
+ normalizedEvent.lease_id,
843
+ normalizedEvent.result,
844
+ normalizedEvent.target,
845
+ JSON.stringify(normalizedEvent.detail || {}),
846
+ this.now()
847
+ );
848
+ return true;
849
+ }
850
+
851
+ async listAuthEvents(options = {}) {
852
+ const limit = normalizeInteger(options.limit, 50);
853
+
854
+ if (this._useMemoryBackend()) {
855
+ const rows = [...this._memory.auth_events]
856
+ .sort((left, right) => (Date.parse(right.event_timestamp || '') || 0) - (Date.parse(left.event_timestamp || '') || 0))
857
+ .map((row) => ({
858
+ ...row,
859
+ detail: row.detail && typeof row.detail === 'object' ? row.detail : {}
860
+ }));
861
+ return limit > 0 ? rows.slice(0, limit) : rows;
862
+ }
863
+
864
+ if (!await this.ensureReady()) {
865
+ return null;
866
+ }
867
+
868
+ const query = limit > 0
869
+ ? `
870
+ SELECT event_id, event_timestamp, event_type, action, actor, lease_id, result, target, detail_json, created_at
871
+ FROM auth_event_stream
872
+ ORDER BY event_timestamp DESC
873
+ LIMIT ?
874
+ `
875
+ : `
876
+ SELECT event_id, event_timestamp, event_type, action, actor, lease_id, result, target, detail_json, created_at
877
+ FROM auth_event_stream
878
+ ORDER BY event_timestamp DESC
879
+ `;
880
+
881
+ const statement = this._db.prepare(query);
882
+ const rows = limit > 0 ? statement.all(limit) : statement.all();
883
+ return rows
884
+ .map((row) => this._mapAuthEventRow(row))
885
+ .filter(Boolean);
886
+ }
887
+
513
888
  _resolveOrCreateTaskRefInMemory(options = {}) {
514
889
  const sceneId = normalizeString(options.sceneId);
515
890
  const specId = normalizeString(options.specId);
@@ -566,6 +941,30 @@ class SceStateStore {
566
941
  this._memory.refs[taskRef] = row;
567
942
  return { ...row, metadata: { ...(row.metadata || {}) } };
568
943
  }
944
+
945
+ _issueAuthLeaseInMemory(options = {}) {
946
+ const leaseId = normalizeString(options.leaseId)
947
+ || `lease-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
948
+ const row = {
949
+ lease_id: leaseId,
950
+ subject: normalizeString(options.subject) || 'unknown',
951
+ role: normalizeString(options.role) || 'maintainer',
952
+ scope: normalizeStringArray(options.scope, ['project:*']),
953
+ reason: normalizeString(options.reason) || null,
954
+ metadata: options.metadata && typeof options.metadata === 'object' ? { ...options.metadata } : {},
955
+ issued_at: normalizeIsoTimestamp(options.issuedAt, this.now()) || this.now(),
956
+ expires_at: normalizeIsoTimestamp(options.expiresAt, this.now()) || this.now(),
957
+ revoked_at: null,
958
+ created_at: normalizeIsoTimestamp(options.nowIso, this.now()) || this.now(),
959
+ updated_at: normalizeIsoTimestamp(options.nowIso, this.now()) || this.now()
960
+ };
961
+ this._memory.auth_leases[leaseId] = row;
962
+ return {
963
+ ...row,
964
+ scope: normalizeStringArray(row.scope, ['project:*']),
965
+ metadata: { ...(row.metadata || {}) }
966
+ };
967
+ }
569
968
  }
570
969
 
571
970
  const STORE_CACHE = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scene-capability-engine",
3
- "version": "3.6.2",
3
+ "version": "3.6.3",
4
4
  "description": "SCE (Scene Capability Engine) - A CLI tool and npm package for spec-driven development with AI coding assistants.",
5
5
  "main": "index.js",
6
6
  "bin": {