@skillsmith/core 0.8.0 → 0.8.1

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 (241) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/src/analytics/ROIDashboardService.compute.d.ts +17 -0
  4. package/dist/src/analytics/ROIDashboardService.compute.d.ts.map +1 -0
  5. package/dist/src/analytics/ROIDashboardService.compute.js +134 -0
  6. package/dist/src/analytics/ROIDashboardService.compute.js.map +1 -0
  7. package/dist/src/analytics/ROIDashboardService.d.ts +0 -8
  8. package/dist/src/analytics/ROIDashboardService.d.ts.map +1 -1
  9. package/dist/src/analytics/ROIDashboardService.js +9 -138
  10. package/dist/src/analytics/ROIDashboardService.js.map +1 -1
  11. package/dist/src/api/client.types.d.ts +5 -0
  12. package/dist/src/api/client.types.d.ts.map +1 -1
  13. package/dist/src/api/index.d.ts +1 -0
  14. package/dist/src/api/index.d.ts.map +1 -1
  15. package/dist/src/api/index.js +4 -0
  16. package/dist/src/api/index.js.map +1 -1
  17. package/dist/src/api/schemas.compatibility.test.d.ts +11 -0
  18. package/dist/src/api/schemas.compatibility.test.d.ts.map +1 -0
  19. package/dist/src/api/schemas.compatibility.test.js +38 -0
  20. package/dist/src/api/schemas.compatibility.test.js.map +1 -0
  21. package/dist/src/api/schemas.d.ts +16 -9
  22. package/dist/src/api/schemas.d.ts.map +1 -1
  23. package/dist/src/api/schemas.js +13 -4
  24. package/dist/src/api/schemas.js.map +1 -1
  25. package/dist/src/api/schemas.license.test.d.ts +11 -0
  26. package/dist/src/api/schemas.license.test.d.ts.map +1 -0
  27. package/dist/src/api/schemas.license.test.js +51 -0
  28. package/dist/src/api/schemas.license.test.js.map +1 -0
  29. package/dist/src/api/types.d.ts +18 -3
  30. package/dist/src/api/types.d.ts.map +1 -1
  31. package/dist/src/api/types.js +29 -2
  32. package/dist/src/api/types.js.map +1 -1
  33. package/dist/src/audit/remote-audit.d.ts +41 -0
  34. package/dist/src/audit/remote-audit.d.ts.map +1 -1
  35. package/dist/src/audit/remote-audit.js +71 -28
  36. package/dist/src/audit/remote-audit.js.map +1 -1
  37. package/dist/src/audit/remote-audit.test.js +89 -1
  38. package/dist/src/audit/remote-audit.test.js.map +1 -1
  39. package/dist/src/compatibility/slugs.d.ts +34 -0
  40. package/dist/src/compatibility/slugs.d.ts.map +1 -0
  41. package/dist/src/compatibility/slugs.js +57 -0
  42. package/dist/src/compatibility/slugs.js.map +1 -0
  43. package/dist/src/db/schema-sql.d.ts +1 -1
  44. package/dist/src/db/schema-sql.d.ts.map +1 -1
  45. package/dist/src/db/schema-sql.js +1 -1
  46. package/dist/src/exports/types.d.ts +1 -0
  47. package/dist/src/exports/types.d.ts.map +1 -1
  48. package/dist/src/exports/types.js +4 -0
  49. package/dist/src/exports/types.js.map +1 -1
  50. package/dist/src/index.d.ts +6 -3
  51. package/dist/src/index.d.ts.map +1 -1
  52. package/dist/src/index.js +7 -3
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/indexer/GitHubIndexer.d.ts.map +1 -1
  55. package/dist/src/indexer/GitHubIndexer.js +1 -1
  56. package/dist/src/indexer/GitHubIndexer.js.map +1 -1
  57. package/dist/src/provenance/SourceRecoveryService.d.ts +48 -0
  58. package/dist/src/provenance/SourceRecoveryService.d.ts.map +1 -0
  59. package/dist/src/provenance/SourceRecoveryService.js +182 -0
  60. package/dist/src/provenance/SourceRecoveryService.js.map +1 -0
  61. package/dist/src/provenance/backfill.d.ts +47 -0
  62. package/dist/src/provenance/backfill.d.ts.map +1 -0
  63. package/dist/src/provenance/backfill.js +206 -0
  64. package/dist/src/provenance/backfill.js.map +1 -0
  65. package/dist/src/provenance/git-config.d.ts +22 -0
  66. package/dist/src/provenance/git-config.d.ts.map +1 -0
  67. package/dist/src/provenance/git-config.js +117 -0
  68. package/dist/src/provenance/git-config.js.map +1 -0
  69. package/dist/src/provenance/index.d.ts +12 -0
  70. package/dist/src/provenance/index.d.ts.map +1 -0
  71. package/dist/src/provenance/index.js +12 -0
  72. package/dist/src/provenance/index.js.map +1 -0
  73. package/dist/src/provenance/local-skill-scan.d.ts +35 -0
  74. package/dist/src/provenance/local-skill-scan.d.ts.map +1 -0
  75. package/dist/src/provenance/local-skill-scan.js +79 -0
  76. package/dist/src/provenance/local-skill-scan.js.map +1 -0
  77. package/dist/src/provenance/plugin-manifest.d.ts +16 -0
  78. package/dist/src/provenance/plugin-manifest.d.ts.map +1 -0
  79. package/dist/src/provenance/plugin-manifest.js +52 -0
  80. package/dist/src/provenance/plugin-manifest.js.map +1 -0
  81. package/dist/src/provenance/types.d.ts +86 -0
  82. package/dist/src/provenance/types.d.ts.map +1 -0
  83. package/dist/src/provenance/types.js +21 -0
  84. package/dist/src/provenance/types.js.map +1 -0
  85. package/dist/src/repositories/SkillRepository.d.ts.map +1 -1
  86. package/dist/src/repositories/SkillRepository.js +3 -1
  87. package/dist/src/repositories/SkillRepository.js.map +1 -1
  88. package/dist/src/scripts/__tests__/scan-imported-skills.test.js +10 -0
  89. package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
  90. package/dist/src/scripts/validation/normalizers.d.ts.map +1 -1
  91. package/dist/src/scripts/validation/normalizers.js +9 -7
  92. package/dist/src/scripts/validation/normalizers.js.map +1 -1
  93. package/dist/src/scripts/validation/types.d.ts +8 -2
  94. package/dist/src/scripts/validation/types.d.ts.map +1 -1
  95. package/dist/src/scripts/validation/types.js +10 -2
  96. package/dist/src/scripts/validation/types.js.map +1 -1
  97. package/dist/src/security/scanner/SecurityScanner.d.ts +0 -7
  98. package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
  99. package/dist/src/security/scanner/SecurityScanner.exec.d.ts +56 -0
  100. package/dist/src/security/scanner/SecurityScanner.exec.d.ts.map +1 -0
  101. package/dist/src/security/scanner/SecurityScanner.exec.js +255 -0
  102. package/dist/src/security/scanner/SecurityScanner.exec.js.map +1 -0
  103. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
  104. package/dist/src/security/scanner/SecurityScanner.helpers.js +17 -1
  105. package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
  106. package/dist/src/security/scanner/SecurityScanner.js +34 -199
  107. package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
  108. package/dist/src/security/scanner/SecurityScanner.scanners.d.ts +22 -0
  109. package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -0
  110. package/dist/src/security/scanner/SecurityScanner.scanners.js +213 -0
  111. package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -0
  112. package/dist/src/security/scanner/index.d.ts +1 -1
  113. package/dist/src/security/scanner/index.d.ts.map +1 -1
  114. package/dist/src/security/scanner/index.js +1 -1
  115. package/dist/src/security/scanner/index.js.map +1 -1
  116. package/dist/src/security/scanner/patterns.d.ts +21 -0
  117. package/dist/src/security/scanner/patterns.d.ts.map +1 -1
  118. package/dist/src/security/scanner/patterns.js +34 -0
  119. package/dist/src/security/scanner/patterns.js.map +1 -1
  120. package/dist/src/security/scanner/types.d.ts +3 -1
  121. package/dist/src/security/scanner/types.d.ts.map +1 -1
  122. package/dist/src/security/scanner/weights.d.ts.map +1 -1
  123. package/dist/src/security/scanner/weights.js +6 -0
  124. package/dist/src/security/scanner/weights.js.map +1 -1
  125. package/dist/src/services/SearchService.helpers.d.ts.map +1 -1
  126. package/dist/src/services/SearchService.helpers.js +2 -0
  127. package/dist/src/services/SearchService.helpers.js.map +1 -1
  128. package/dist/src/services/SearchService.types.d.ts +1 -0
  129. package/dist/src/services/SearchService.types.d.ts.map +1 -1
  130. package/dist/src/services/skill-installation.helpers.d.ts +2 -26
  131. package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
  132. package/dist/src/services/skill-installation.helpers.js +2 -120
  133. package/dist/src/services/skill-installation.helpers.js.map +1 -1
  134. package/dist/src/services/skill-installation.io.d.ts +43 -0
  135. package/dist/src/services/skill-installation.io.d.ts.map +1 -0
  136. package/dist/src/services/skill-installation.io.js +176 -0
  137. package/dist/src/services/skill-installation.io.js.map +1 -0
  138. package/dist/src/services/skill-installation.service.d.ts.map +1 -1
  139. package/dist/src/services/skill-installation.service.js +51 -113
  140. package/dist/src/services/skill-installation.service.js.map +1 -1
  141. package/dist/src/services/skill-installation.types.d.ts.map +1 -1
  142. package/dist/src/services/skill-installation.types.js +9 -0
  143. package/dist/src/services/skill-installation.types.js.map +1 -1
  144. package/dist/src/services/skill-installation.validate.d.ts +44 -0
  145. package/dist/src/services/skill-installation.validate.d.ts.map +1 -0
  146. package/dist/src/services/skill-installation.validate.js +134 -0
  147. package/dist/src/services/skill-installation.validate.js.map +1 -0
  148. package/dist/src/telemetry/wrap.d.ts +1 -1
  149. package/dist/src/telemetry/wrap.d.ts.map +1 -1
  150. package/dist/src/telemetry/wrap.js.map +1 -1
  151. package/dist/src/types/skill.d.ts +6 -1
  152. package/dist/src/types/skill.d.ts.map +1 -1
  153. package/dist/src/types/skill.js.map +1 -1
  154. package/dist/src/types.d.ts +25 -1
  155. package/dist/src/types.d.ts.map +1 -1
  156. package/dist/src/types.js +3 -1
  157. package/dist/src/types.js.map +1 -1
  158. package/dist/tests/GitHubIndexer.edge-cases.test.js +2 -2
  159. package/dist/tests/GitHubIndexer.repository.test.d.ts +10 -0
  160. package/dist/tests/GitHubIndexer.repository.test.d.ts.map +1 -0
  161. package/dist/tests/GitHubIndexer.repository.test.js +190 -0
  162. package/dist/tests/GitHubIndexer.repository.test.js.map +1 -0
  163. package/dist/tests/GitHubIndexer.test.d.ts +2 -1
  164. package/dist/tests/GitHubIndexer.test.d.ts.map +1 -1
  165. package/dist/tests/GitHubIndexer.test.js +5 -183
  166. package/dist/tests/GitHubIndexer.test.js.map +1 -1
  167. package/dist/tests/QuarantineRepository.review-state-machine.test.d.ts +14 -0
  168. package/dist/tests/QuarantineRepository.review-state-machine.test.d.ts.map +1 -0
  169. package/dist/tests/QuarantineRepository.review-state-machine.test.js +296 -0
  170. package/dist/tests/QuarantineRepository.review-state-machine.test.js.map +1 -0
  171. package/dist/tests/RawUrlSourceAdapter.security.test.js +3 -2
  172. package/dist/tests/RawUrlSourceAdapter.security.test.js.map +1 -1
  173. package/dist/tests/SecurityScanner.exec.test.d.ts +11 -0
  174. package/dist/tests/SecurityScanner.exec.test.d.ts.map +1 -0
  175. package/dist/tests/SecurityScanner.exec.test.js +190 -0
  176. package/dist/tests/SecurityScanner.exec.test.js.map +1 -0
  177. package/dist/tests/SwarmIndexer.test.js +2 -2
  178. package/dist/tests/api/client.validation.test.js +7 -4
  179. package/dist/tests/api/client.validation.test.js.map +1 -1
  180. package/dist/tests/fixtures/api-responses/index.js +1 -1
  181. package/dist/tests/fixtures/api-responses/index.js.map +1 -1
  182. package/dist/tests/integration/QuarantineService.concurrency.test.d.ts +34 -0
  183. package/dist/tests/integration/QuarantineService.concurrency.test.d.ts.map +1 -0
  184. package/dist/tests/integration/QuarantineService.concurrency.test.js +280 -0
  185. package/dist/tests/integration/QuarantineService.concurrency.test.js.map +1 -0
  186. package/dist/tests/integration/openapi-contract.test.d.ts +16 -0
  187. package/dist/tests/integration/openapi-contract.test.d.ts.map +1 -0
  188. package/dist/tests/integration/openapi-contract.test.js +231 -0
  189. package/dist/tests/integration/openapi-contract.test.js.map +1 -0
  190. package/dist/tests/provenance/SourceRecoveryService.test.d.ts +2 -0
  191. package/dist/tests/provenance/SourceRecoveryService.test.d.ts.map +1 -0
  192. package/dist/tests/provenance/SourceRecoveryService.test.js +132 -0
  193. package/dist/tests/provenance/SourceRecoveryService.test.js.map +1 -0
  194. package/dist/tests/provenance/backfill.test.d.ts +2 -0
  195. package/dist/tests/provenance/backfill.test.d.ts.map +1 -0
  196. package/dist/tests/provenance/backfill.test.js +278 -0
  197. package/dist/tests/provenance/backfill.test.js.map +1 -0
  198. package/dist/tests/provenance/git-config.test.d.ts +2 -0
  199. package/dist/tests/provenance/git-config.test.d.ts.map +1 -0
  200. package/dist/tests/provenance/git-config.test.js +73 -0
  201. package/dist/tests/provenance/git-config.test.js.map +1 -0
  202. package/dist/tests/provenance/local-skill-scan.test.d.ts +2 -0
  203. package/dist/tests/provenance/local-skill-scan.test.d.ts.map +1 -0
  204. package/dist/tests/provenance/local-skill-scan.test.js +51 -0
  205. package/dist/tests/provenance/local-skill-scan.test.js.map +1 -0
  206. package/dist/tests/provenance/plugin-manifest.test.d.ts +2 -0
  207. package/dist/tests/provenance/plugin-manifest.test.d.ts.map +1 -0
  208. package/dist/tests/provenance/plugin-manifest.test.js +49 -0
  209. package/dist/tests/provenance/plugin-manifest.test.js.map +1 -0
  210. package/dist/tests/scripts/import-to-database.test.js +7 -1
  211. package/dist/tests/scripts/import-to-database.test.js.map +1 -1
  212. package/dist/tests/security/scanner-regression-guard.test.js +5 -1
  213. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  214. package/dist/tests/security.test.js +45 -0
  215. package/dist/tests/security.test.js.map +1 -1
  216. package/dist/tests/services/aidefence-feedback.test.js +2 -0
  217. package/dist/tests/services/aidefence-feedback.test.js.map +1 -1
  218. package/dist/tests/services/dep-quarantine-check.test.js +1 -1
  219. package/dist/tests/services/dep-quarantine-check.test.js.map +1 -1
  220. package/dist/tests/skill-scanner/allowlist.test.js +8 -1
  221. package/dist/tests/skill-scanner/allowlist.test.js.map +1 -1
  222. package/dist/tests/unit/services/skill-installation.gap1.test.d.ts +12 -0
  223. package/dist/tests/unit/services/skill-installation.gap1.test.d.ts.map +1 -0
  224. package/dist/tests/unit/services/skill-installation.gap1.test.js +96 -0
  225. package/dist/tests/unit/services/skill-installation.gap1.test.js.map +1 -0
  226. package/dist/tests/unit/services/skill-installation.io.test.d.ts +11 -0
  227. package/dist/tests/unit/services/skill-installation.io.test.d.ts.map +1 -0
  228. package/dist/tests/unit/services/skill-installation.io.test.js +130 -0
  229. package/dist/tests/unit/services/skill-installation.io.test.js.map +1 -0
  230. package/dist/tests/validate-skills.dedup.test.d.ts +5 -0
  231. package/dist/tests/validate-skills.dedup.test.d.ts.map +1 -0
  232. package/dist/tests/validate-skills.dedup.test.js +241 -0
  233. package/dist/tests/validate-skills.dedup.test.js.map +1 -0
  234. package/dist/tests/validate-skills.normalizers.test.d.ts +5 -0
  235. package/dist/tests/validate-skills.normalizers.test.d.ts.map +1 -0
  236. package/dist/tests/validate-skills.normalizers.test.js +109 -0
  237. package/dist/tests/validate-skills.normalizers.test.js.map +1 -0
  238. package/dist/tests/validate-skills.test.d.ts +1 -1
  239. package/dist/tests/validate-skills.test.js +7 -345
  240. package/dist/tests/validate-skills.test.js.map +1 -1
  241. package/package.json +19 -8
@@ -0,0 +1,280 @@
1
+ /**
2
+ * SMI-5358: QuarantineService Concurrency / Race Tests
3
+ *
4
+ * Existing coverage (QuarantineService.test.ts) only exercises single, strictly
5
+ * sequential reviews. These tests model OVERLAPPING reviewer/writer operations
6
+ * against the SAME quarantine entry and assert the concurrency invariants that
7
+ * protect against lost updates and illegal state transitions:
8
+ *
9
+ * 1. Two reviewers racing to decide one SUSPICIOUS entry (approve vs reject)
10
+ * -> exactly one wins, the loser is rejected as ALREADY_REVIEWED, and the
11
+ * persisted DB state reflects the winner's decision (no lost update).
12
+ * 2. A stale-snapshot ("optimistic version") writer whose read predates another
13
+ * reviewer's decision -> the stale write is rejected, persisted state is
14
+ * unchanged from the winner.
15
+ * 3. MALICIOUS double-approval by the SAME reviewer -> the second is rejected,
16
+ * only one approval row exists, and the entry does NOT illegally transition.
17
+ * 4. MALICIOUS approval by two DISTINCT reviewers reaching the required count
18
+ * -> the entry transitions to approved EXACTLY once with exactly two
19
+ * approval rows (no double-count, no double-transition).
20
+ *
21
+ * HONESTY NOTE: better-sqlite3 is fully SYNCHRONOUS, so genuine OS-thread
22
+ * interleaving is not achievable in-process. `Promise.all` / `Promise.allSettled`
23
+ * here schedule each synchronous `service.review()` as a microtask; each call
24
+ * therefore runs to completion atomically rather than truly interleaving. We are
25
+ * NOT faking parallelism. What we test is the REAL concurrency invariant: the
26
+ * read-then-act guard (`reviewStatus !== 'pending'`), the per-reviewer guard
27
+ * (`hasReviewerApproved`), and the approval-count check are what prevent lost
28
+ * updates / illegal transitions when overlapping operations target the same
29
+ * entry. All assertions read the PERSISTED DB state after the race, never a mock
30
+ * return value. Removing any of those guards makes the persisted state
31
+ * inconsistent and fails these tests.
32
+ */
33
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
34
+ import { QuarantineService } from '../../src/services/quarantine/QuarantineService.js';
35
+ import { QuarantineServiceError } from '../../src/services/quarantine/types.js';
36
+ import { QuarantineRepository } from '../../src/repositories/quarantine/index.js';
37
+ import { ApprovalRepository } from '../../src/repositories/quarantine/index.js';
38
+ import { AuditLogger } from '../../src/security/AuditLogger.js';
39
+ import { createDatabaseSync } from '../../src/db/createDatabase.js';
40
+ // ============================================================================
41
+ // Test Helpers
42
+ // ============================================================================
43
+ const REVIEW_PERMS = ['quarantine:read', 'quarantine:review'];
44
+ const MALICIOUS_PERMS = [
45
+ 'quarantine:read',
46
+ 'quarantine:review',
47
+ 'quarantine:review_malicious',
48
+ ];
49
+ function makeSession(overrides = {}) {
50
+ return {
51
+ userId: 'user-123',
52
+ email: 'reviewer@example.com',
53
+ displayName: 'Test Reviewer',
54
+ permissions: REVIEW_PERMS,
55
+ sessionId: 'session-456',
56
+ expiresAt: new Date(Date.now() + 3600000),
57
+ ...overrides,
58
+ };
59
+ }
60
+ /** Run `fn` on a fresh microtask so overlapping calls are scheduled together. */
61
+ function defer(fn) {
62
+ return Promise.resolve().then(fn);
63
+ }
64
+ function fulfilled(results) {
65
+ return results.filter((r) => r.status === 'fulfilled');
66
+ }
67
+ function rejectedWith(results) {
68
+ return results.filter((r) => r.status === 'rejected');
69
+ }
70
+ // ============================================================================
71
+ // Test Suite
72
+ // ============================================================================
73
+ describe('SMI-5358: QuarantineService concurrency / race invariants', () => {
74
+ let db;
75
+ let repository;
76
+ let approvalRepository;
77
+ let auditLogger;
78
+ let service;
79
+ beforeEach(() => {
80
+ db = createDatabaseSync(':memory:');
81
+ auditLogger = new AuditLogger(db);
82
+ repository = new QuarantineRepository(db, auditLogger);
83
+ approvalRepository = new ApprovalRepository(db);
84
+ service = new QuarantineService(repository, approvalRepository, auditLogger);
85
+ });
86
+ afterEach(() => {
87
+ db.close();
88
+ });
89
+ // ==========================================================================
90
+ // SUSPICIOUS: approve vs reject race on the same entry
91
+ // ==========================================================================
92
+ describe('SUSPICIOUS approve-vs-reject race', () => {
93
+ it('lets exactly one reviewer win; the loser is rejected ALREADY_REVIEWED and no update is lost', async () => {
94
+ const approver = makeSession({ userId: 'user-approve', email: 'approve@example.com' });
95
+ const rejecter = makeSession({ userId: 'user-reject', email: 'reject@example.com' });
96
+ const entry = repository.create({
97
+ skillId: 'test/race-skill',
98
+ source: 'test',
99
+ quarantineReason: 'Overlapping reviews',
100
+ severity: 'SUSPICIOUS',
101
+ });
102
+ const results = await Promise.allSettled([
103
+ defer(() => service.review(approver, entry.id, {
104
+ reviewStatus: 'approved',
105
+ reviewNotes: 'approve path',
106
+ })),
107
+ defer(() => service.review(rejecter, entry.id, {
108
+ reviewStatus: 'rejected',
109
+ reviewNotes: 'reject path',
110
+ })),
111
+ ]);
112
+ const winners = fulfilled(results);
113
+ const losers = rejectedWith(results);
114
+ // Exactly one decision succeeds, exactly one is blocked.
115
+ expect(winners.length).toBe(1);
116
+ expect(losers.length).toBe(1);
117
+ expect(losers[0].reason).toBeInstanceOf(QuarantineServiceError);
118
+ expect(losers[0].reason.code).toBe('ALREADY_REVIEWED');
119
+ // Persisted state reflects the WINNER, not a half-written / overwritten row.
120
+ const persisted = repository.findById(entry.id);
121
+ expect(persisted).not.toBeNull();
122
+ expect(persisted.reviewStatus).not.toBe('pending');
123
+ const winner = winners[0].value;
124
+ if (winner.approved) {
125
+ expect(persisted.reviewStatus).toBe('approved');
126
+ expect(persisted.reviewedBy).toBe(approver.email);
127
+ }
128
+ else {
129
+ expect(persisted.reviewStatus).toBe('rejected');
130
+ expect(persisted.reviewedBy).toBe(rejecter.email);
131
+ }
132
+ });
133
+ it('rejects a stale-snapshot writer whose read predates the winning decision (no lost update)', () => {
134
+ const winner = makeSession({ userId: 'user-winner', email: 'winner@example.com' });
135
+ const staleWriter = makeSession({ userId: 'user-stale', email: 'stale@example.com' });
136
+ const entry = repository.create({
137
+ skillId: 'test/stale-skill',
138
+ source: 'test',
139
+ quarantineReason: 'Stale snapshot write',
140
+ severity: 'SUSPICIOUS',
141
+ });
142
+ // staleWriter captures a snapshot while the entry is still pending.
143
+ const snapshot = service.findById(staleWriter, entry.id);
144
+ expect(snapshot).not.toBeNull();
145
+ expect(snapshot.reviewStatus).toBe('pending');
146
+ // winner commits a decision first.
147
+ service.review(winner, entry.id, {
148
+ reviewStatus: 'approved',
149
+ reviewNotes: 'committed first',
150
+ });
151
+ // staleWriter now tries to write based on its stale (pending) view.
152
+ expect(() => service.review(staleWriter, entry.id, {
153
+ reviewStatus: 'rejected',
154
+ reviewNotes: 'based on stale read',
155
+ })).toThrow(QuarantineServiceError);
156
+ // The winner's decision survives untouched: the stale reject did not overwrite it.
157
+ const persisted = repository.findById(entry.id);
158
+ expect(persisted.reviewStatus).toBe('approved');
159
+ expect(persisted.reviewedBy).toBe(winner.email);
160
+ });
161
+ });
162
+ // ==========================================================================
163
+ // MALICIOUS: same-reviewer double-approval (per-reviewer guard)
164
+ // ==========================================================================
165
+ describe('MALICIOUS same-reviewer double-approval', () => {
166
+ it('does not double-count or illegally transition when one reviewer approves twice', async () => {
167
+ const reviewer = makeSession({
168
+ userId: 'user-dup',
169
+ email: 'dup@example.com',
170
+ permissions: MALICIOUS_PERMS,
171
+ });
172
+ const entry = repository.create({
173
+ skillId: 'test/malicious-dup',
174
+ source: 'test',
175
+ quarantineReason: 'Malicious code detected',
176
+ severity: 'MALICIOUS',
177
+ });
178
+ const results = await Promise.allSettled([
179
+ defer(() => service.review(reviewer, entry.id, {
180
+ reviewStatus: 'approved',
181
+ reviewNotes: 'first',
182
+ })),
183
+ defer(() => service.review(reviewer, entry.id, {
184
+ reviewStatus: 'approved',
185
+ reviewNotes: 'second (duplicate)',
186
+ })),
187
+ ]);
188
+ const winners = fulfilled(results);
189
+ const losers = rejectedWith(results);
190
+ // One approval recorded, the duplicate rejected.
191
+ expect(winners.length).toBe(1);
192
+ expect(losers.length).toBe(1);
193
+ expect(losers[0].reason.code).toBe('ALREADY_REVIEWED');
194
+ // The single accepted approval did NOT complete the workflow.
195
+ expect(winners[0].value.approved).toBe(false);
196
+ // Persisted: exactly ONE approval row, and the entry stays pending.
197
+ const pending = approvalRepository.getPendingApprovals(entry.id);
198
+ expect(pending.length).toBe(1);
199
+ expect(pending[0].reviewerId).toBe('user-dup');
200
+ const persisted = repository.findById(entry.id);
201
+ expect(persisted.reviewStatus).toBe('pending');
202
+ expect(persisted.reviewedBy).toBeNull();
203
+ });
204
+ });
205
+ // ==========================================================================
206
+ // MALICIOUS: two distinct reviewers reaching the required count
207
+ // ==========================================================================
208
+ describe('MALICIOUS two-reviewer approval completion', () => {
209
+ it('transitions to approved exactly once with exactly two approval rows (no double-count)', async () => {
210
+ const reviewerA = makeSession({
211
+ userId: 'user-A',
212
+ email: 'a@example.com',
213
+ permissions: MALICIOUS_PERMS,
214
+ });
215
+ const reviewerB = makeSession({
216
+ userId: 'user-B',
217
+ email: 'b@example.com',
218
+ permissions: MALICIOUS_PERMS,
219
+ });
220
+ const entry = repository.create({
221
+ skillId: 'test/malicious-two',
222
+ source: 'test',
223
+ quarantineReason: 'Malicious code detected',
224
+ severity: 'MALICIOUS',
225
+ });
226
+ const results = await Promise.allSettled([
227
+ defer(() => service.review(reviewerA, entry.id, {
228
+ reviewStatus: 'approved',
229
+ reviewNotes: 'approval A',
230
+ })),
231
+ defer(() => service.review(reviewerB, entry.id, {
232
+ reviewStatus: 'approved',
233
+ reviewNotes: 'approval B',
234
+ })),
235
+ ]);
236
+ const winners = fulfilled(results);
237
+ // Both distinct reviewers are accepted.
238
+ expect(winners.length).toBe(2);
239
+ // Exactly ONE result reports completion (approved), the other in-progress.
240
+ const completed = winners.filter((r) => r.value.approved === true);
241
+ const inProgress = winners.filter((r) => r.value.approved === false);
242
+ expect(completed.length).toBe(1);
243
+ expect(inProgress.length).toBe(1);
244
+ // Persisted: exactly two approval rows, both approvals, all marked complete.
245
+ const allApprovals = approvalRepository.getApprovals(entry.id);
246
+ expect(allApprovals.length).toBe(2);
247
+ expect(allApprovals.filter((a) => a.decision === 'approved').length).toBe(2);
248
+ expect(allApprovals.every((a) => a.isComplete)).toBe(true);
249
+ const reviewerIds = allApprovals.map((a) => a.reviewerId).sort();
250
+ expect(reviewerIds).toEqual(['user-A', 'user-B']);
251
+ // No pending approvals remain, and the entry transitioned to approved once.
252
+ expect(approvalRepository.getPendingApprovals(entry.id).length).toBe(0);
253
+ const persisted = repository.findById(entry.id);
254
+ expect(persisted.reviewStatus).toBe('approved');
255
+ });
256
+ it('keeps the entry pending after a single approval (count guard holds)', () => {
257
+ const reviewerA = makeSession({
258
+ userId: 'user-solo',
259
+ email: 'solo@example.com',
260
+ permissions: MALICIOUS_PERMS,
261
+ });
262
+ const entry = repository.create({
263
+ skillId: 'test/malicious-solo',
264
+ source: 'test',
265
+ quarantineReason: 'Malicious code detected',
266
+ severity: 'MALICIOUS',
267
+ });
268
+ const result = service.review(reviewerA, entry.id, {
269
+ reviewStatus: 'approved',
270
+ reviewNotes: 'only approval',
271
+ });
272
+ // A single approval must NOT unilaterally unquarantine a MALICIOUS skill.
273
+ expect(result.approved).toBe(false);
274
+ const persisted = repository.findById(entry.id);
275
+ expect(persisted.reviewStatus).toBe('pending');
276
+ expect(approvalRepository.getPendingApprovals(entry.id).length).toBe(1);
277
+ });
278
+ });
279
+ });
280
+ //# sourceMappingURL=QuarantineService.concurrency.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"QuarantineService.concurrency.test.js","sourceRoot":"","sources":["../../../tests/integration/QuarantineService.concurrency.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SMI-5213 / Wave 1 BARRIER: OpenAPI contract enforcement test.
3
+ *
4
+ * Guards the three-way consistency between:
5
+ * 1. docs/internal/api/openapi.yaml (source of truth)
6
+ * 2. packages/core/src/api/types.ts (API_TRUST_TIERS, API_CATEGORIES)
7
+ * 3. supabase/config.toml (verify_jwt = false function registrations)
8
+ *
9
+ * Skip-gated: the entire suite is skipped when docs/internal/api/openapi.yaml
10
+ * is absent (external contributors / CI without the docs/internal submodule).
11
+ *
12
+ * verify_jwt=false (21 fns) ⊋ documented-public (12 paths).
13
+ * The allowlist below is the positive source of truth for the public surface.
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=openapi-contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-contract.test.d.ts","sourceRoot":"","sources":["../../../tests/integration/openapi-contract.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
@@ -0,0 +1,231 @@
1
+ /**
2
+ * SMI-5213 / Wave 1 BARRIER: OpenAPI contract enforcement test.
3
+ *
4
+ * Guards the three-way consistency between:
5
+ * 1. docs/internal/api/openapi.yaml (source of truth)
6
+ * 2. packages/core/src/api/types.ts (API_TRUST_TIERS, API_CATEGORIES)
7
+ * 3. supabase/config.toml (verify_jwt = false function registrations)
8
+ *
9
+ * Skip-gated: the entire suite is skipped when docs/internal/api/openapi.yaml
10
+ * is absent (external contributors / CI without the docs/internal submodule).
11
+ *
12
+ * verify_jwt=false (21 fns) ⊋ documented-public (12 paths).
13
+ * The allowlist below is the positive source of truth for the public surface.
14
+ */
15
+ import { describe, it, expect } from 'vitest';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { fileURLToPath } from 'node:url';
18
+ import path from 'node:path';
19
+ import { parse } from 'yaml';
20
+ import { API_TRUST_TIERS, API_CATEGORIES } from '../../src/api/types.js';
21
+ // ---------------------------------------------------------------------------
22
+ // Locate files relative to this test file
23
+ // ---------------------------------------------------------------------------
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = path.dirname(__filename);
26
+ // packages/core/tests/integration/ → up 4 dirs to repo root
27
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
28
+ const OPENAPI_PATH = path.join(REPO_ROOT, 'docs', 'internal', 'api', 'openapi.yaml');
29
+ const SUPABASE_CONFIG_PATH = path.join(REPO_ROOT, 'supabase', 'config.toml');
30
+ // SMI-5226: the spec is committed to the website's public/ dir so Vercel's
31
+ // remote rebuild serves it at /openapi.yaml (the deploy can't see the private
32
+ // docs/internal submodule). This committed copy must stay byte-identical to the
33
+ // source. The dedicated PR check openapi-spec-sync.yml is the enforced guard
34
+ // (the vitest CI job does not fetch docs/internal); this is local-dev signal.
35
+ const WEBSITE_SPEC_PATH = path.join(REPO_ROOT, 'packages', 'website', 'public', 'openapi.yaml');
36
+ // ---------------------------------------------------------------------------
37
+ // Allowlist of the 12 documented public endpoints (positive source of truth)
38
+ // ---------------------------------------------------------------------------
39
+ const DOCUMENTED_ENDPOINTS = [
40
+ '/skills-search',
41
+ '/skills-get',
42
+ '/skills-recommend',
43
+ '/events',
44
+ '/health',
45
+ '/stats',
46
+ '/early-access-signup',
47
+ '/contact-submit',
48
+ '/checkout',
49
+ '/stripe-webhook',
50
+ '/auth-device-code',
51
+ '/auth-device-token',
52
+ ];
53
+ // Gateway-verified functions (deploy WITHOUT --no-verify-jwt); they appear in
54
+ // config.toml with verify_jwt = true and are NOT in the allowlist above but
55
+ // may be reachable via other surfaces.
56
+ const GATEWAY_VERIFIED_EXCEPTIONS = new Set([
57
+ 'webhook-dlq',
58
+ 'auth-device-approve',
59
+ 'auth-device-preview',
60
+ 'indexer-dispatch',
61
+ 'team-invite-send',
62
+ 'sync-stripe-email',
63
+ 'sync-oauth-email',
64
+ ]);
65
+ function collectEnums(node, breadcrumb) {
66
+ if (!node || typeof node !== 'object')
67
+ return [];
68
+ const results = [];
69
+ if (Array.isArray(node)) {
70
+ for (let i = 0; i < node.length; i++) {
71
+ results.push(...collectEnums(node[i], `${breadcrumb}[${i}]`));
72
+ }
73
+ return results;
74
+ }
75
+ const obj = node;
76
+ // Parameter-style: { name: 'trust_tier', schema: { enum: [...] } }
77
+ if (typeof obj['name'] === 'string' &&
78
+ obj['schema'] &&
79
+ typeof obj['schema'] === 'object' &&
80
+ !Array.isArray(obj['schema'])) {
81
+ const schema = obj['schema'];
82
+ if (Array.isArray(schema['enum'])) {
83
+ results.push({
84
+ fieldName: obj['name'],
85
+ enumValues: schema['enum'],
86
+ location: `${breadcrumb}(param:${obj['name']}).schema.enum`,
87
+ });
88
+ }
89
+ }
90
+ // Schema-property style: { properties: { trust_tier: { enum: [...] } } }
91
+ if (obj['properties'] &&
92
+ typeof obj['properties'] === 'object' &&
93
+ !Array.isArray(obj['properties'])) {
94
+ const props = obj['properties'];
95
+ for (const [propName, propVal] of Object.entries(props)) {
96
+ if (propVal &&
97
+ typeof propVal === 'object' &&
98
+ !Array.isArray(propVal) &&
99
+ Array.isArray(propVal['enum'])) {
100
+ results.push({
101
+ fieldName: propName,
102
+ enumValues: propVal['enum'],
103
+ location: `${breadcrumb}.properties.${propName}.enum`,
104
+ });
105
+ }
106
+ // Recurse into the property value
107
+ results.push(...collectEnums(propVal, `${breadcrumb}.properties.${propName}`));
108
+ }
109
+ }
110
+ // Recurse into all other keys (skipping 'properties' already handled and 'schema'
111
+ // only if we didn't already collect from it above — but recurse always to find nested)
112
+ for (const [key, val] of Object.entries(obj)) {
113
+ if (key === 'properties')
114
+ continue; // already handled above
115
+ results.push(...collectEnums(val, `${breadcrumb}.${key}`));
116
+ }
117
+ return results;
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // Deduplicate by (fieldName, location) to avoid double-counting from recursion
121
+ // ---------------------------------------------------------------------------
122
+ function deduplicateEnums(occurrences) {
123
+ const seen = new Set();
124
+ return occurrences.filter((o) => {
125
+ const key = `${o.fieldName}::${o.location}`;
126
+ if (seen.has(key))
127
+ return false;
128
+ seen.add(key);
129
+ return true;
130
+ });
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // Parse config.toml: collect function names with verify_jwt = false
134
+ // ---------------------------------------------------------------------------
135
+ function collectNoJwtFunctions(tomlContent) {
136
+ const result = new Set();
137
+ // Match [functions.<name>] blocks followed by verify_jwt = false
138
+ const blockRegex = /\[functions\.([^\]]+)\][^[]*verify_jwt\s*=\s*false/g;
139
+ let match;
140
+ while ((match = blockRegex.exec(tomlContent)) !== null) {
141
+ result.add(match[1].trim());
142
+ }
143
+ return result;
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Conditionally skip when submodule is absent
147
+ // ---------------------------------------------------------------------------
148
+ const specExists = existsSync(OPENAPI_PATH);
149
+ describe.skipIf(!specExists)('SMI-5213: OpenAPI contract', () => {
150
+ // Parse once per suite
151
+ const specContent = specExists ? readFileSync(OPENAPI_PATH, 'utf-8') : '';
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ const spec = specExists ? parse(specContent) : null;
154
+ // -------------------------------------------------------------------------
155
+ // Top-level metadata assertions
156
+ // -------------------------------------------------------------------------
157
+ it('license is Elastic License 2.0', () => {
158
+ expect(spec.info.license.name).toBe('Elastic License 2.0');
159
+ });
160
+ it('production server URL is https://api.skillsmith.app', () => {
161
+ const servers = spec.servers ?? [];
162
+ const prodServer = servers.find((s) => s.description?.toLowerCase().includes('production'));
163
+ expect(prodServer, 'No production server entry found').toBeDefined();
164
+ expect(prodServer.url).toBe('https://api.skillsmith.app');
165
+ });
166
+ it('contact.url includes smith-horn', () => {
167
+ expect(spec.info.contact.url).toContain('smith-horn');
168
+ });
169
+ // -------------------------------------------------------------------------
170
+ // Enum consistency: trust_tier
171
+ // -------------------------------------------------------------------------
172
+ it('all trust_tier enum occurrences match API_TRUST_TIERS', () => {
173
+ const all = deduplicateEnums(collectEnums(spec, 'spec'));
174
+ const trustTierOccurrences = all.filter((o) => o.fieldName === 'trust_tier');
175
+ expect(trustTierOccurrences.length, `Expected ≥2 trust_tier enum occurrences, found ${trustTierOccurrences.length}: ${trustTierOccurrences.map((o) => o.location).join(', ')}`).toBeGreaterThanOrEqual(2);
176
+ for (const occurrence of trustTierOccurrences) {
177
+ expect(occurrence.enumValues, `trust_tier enum mismatch at ${occurrence.location}`).toEqual([
178
+ ...API_TRUST_TIERS,
179
+ ]);
180
+ }
181
+ });
182
+ it('all category enum occurrences match API_CATEGORIES', () => {
183
+ const all = deduplicateEnums(collectEnums(spec, 'spec'));
184
+ const categoryOccurrences = all.filter((o) => o.fieldName === 'category');
185
+ expect(categoryOccurrences.length, `Expected ≥1 category enum occurrence, found ${categoryOccurrences.length}: ${categoryOccurrences.map((o) => o.location).join(', ')}`).toBeGreaterThanOrEqual(1);
186
+ for (const occurrence of categoryOccurrences) {
187
+ expect(occurrence.enumValues, `category enum mismatch at ${occurrence.location}`).toEqual([
188
+ ...API_CATEGORIES,
189
+ ]);
190
+ }
191
+ });
192
+ // -------------------------------------------------------------------------
193
+ // Documented paths
194
+ // -------------------------------------------------------------------------
195
+ it('spec documents all 12 allowlisted paths', () => {
196
+ const specPaths = Object.keys(spec.paths ?? {});
197
+ for (const endpoint of DOCUMENTED_ENDPOINTS) {
198
+ expect(specPaths, `Spec is missing documented endpoint: ${endpoint}`).toContain(endpoint);
199
+ }
200
+ });
201
+ it('spec contains no undocumented paths beyond the allowlist', () => {
202
+ const specPaths = Object.keys(spec.paths ?? {});
203
+ const allowSet = new Set(DOCUMENTED_ENDPOINTS);
204
+ const extras = specPaths.filter((p) => !allowSet.has(p));
205
+ expect(extras, `Spec contains paths not in the documented allowlist: ${extras.join(', ')}`).toHaveLength(0);
206
+ });
207
+ // -------------------------------------------------------------------------
208
+ // config.toml surface alignment
209
+ // -------------------------------------------------------------------------
210
+ it("each documented endpoint's function name is registered as verify_jwt=false (or is a gateway-verified exception)", () => {
211
+ expect(existsSync(SUPABASE_CONFIG_PATH), 'supabase/config.toml not found').toBe(true);
212
+ const toml = readFileSync(SUPABASE_CONFIG_PATH, 'utf-8');
213
+ const noJwtFunctions = collectNoJwtFunctions(toml);
214
+ for (const endpoint of DOCUMENTED_ENDPOINTS) {
215
+ // Strip leading slash to get function name
216
+ const fnName = endpoint.replace(/^\//, '');
217
+ const isNoJwt = noJwtFunctions.has(fnName);
218
+ const isException = GATEWAY_VERIFIED_EXCEPTIONS.has(fnName);
219
+ expect(isNoJwt || isException, `${endpoint} (fn: ${fnName}) is neither in verify_jwt=false list nor in GATEWAY_VERIFIED_EXCEPTIONS`).toBe(true);
220
+ }
221
+ });
222
+ // -------------------------------------------------------------------------
223
+ // SMI-5226: committed website copy must match the source byte-for-byte
224
+ // -------------------------------------------------------------------------
225
+ it('committed packages/website/public/openapi.yaml is byte-identical to the source', () => {
226
+ expect(existsSync(WEBSITE_SPEC_PATH), 'packages/website/public/openapi.yaml is missing — run `npm run sync:openapi` in packages/website and commit').toBe(true);
227
+ const websiteCopy = readFileSync(WEBSITE_SPEC_PATH, 'utf-8');
228
+ expect(websiteCopy, 'public/openapi.yaml drifted from docs/internal/api/openapi.yaml — run `npm run sync:openapi` in packages/website and commit').toBe(specContent);
229
+ });
230
+ });
231
+ //# sourceMappingURL=openapi-contract.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-contract.test.js","sourceRoot":"","sources":["../../../tests/integration/openapi-contract.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=SourceRecoveryService.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SourceRecoveryService.test.d.ts","sourceRoot":"","sources":["../../../tests/provenance/SourceRecoveryService.test.ts"],"names":[],"mappings":""}