@soulbatical/tetra-dev-toolkit 1.10.2 → 1.12.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.
@@ -24,14 +24,23 @@ export async function run(config, projectRoot) {
24
24
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
25
25
  }
26
26
 
27
- // Check if project uses the systemDB/userDB pattern
28
- const hasSystemDb = existsSync(`${projectRoot}/backend/src/core/systemDb.ts`) ||
29
- existsSync(`${projectRoot}/src/core/systemDb.ts`)
27
+ // Check if project uses the systemDB/userDB pattern (local file OR tetra-core package)
28
+ const hasLocalSystemDb = existsSync(`${projectRoot}/backend/src/core/systemDb.ts`) ||
29
+ existsSync(`${projectRoot}/src/core/systemDb.ts`)
30
30
 
31
- if (!hasSystemDb) {
32
- // Project doesn't use this pattern, skip check
31
+ let hasTetraCore = false
32
+ for (const pkgPath of [`${projectRoot}/package.json`, `${projectRoot}/backend/package.json`]) {
33
+ if (!existsSync(pkgPath)) continue
34
+ try {
35
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
36
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
37
+ if (deps['@soulbatical/tetra-core']) { hasTetraCore = true; break }
38
+ } catch { /* skip */ }
39
+ }
40
+
41
+ if (!hasLocalSystemDb && !hasTetraCore) {
33
42
  results.skipped = true
34
- results.skipReason = 'Project does not use systemDB/userDB pattern'
43
+ results.skipReason = 'Project does not use systemDB/userDB pattern (no local systemDb.ts and no @soulbatical/tetra-core)'
35
44
  return results
36
45
  }
37
46
 
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Mixed Database Usage Detection — HARD BLOCK
3
+ *
4
+ * Controllers MUST use only ONE type of database helper.
5
+ * The DB helper must match the controller naming convention:
6
+ *
7
+ * AdminXxxController → adminDB(req) only
8
+ * UserXxxController → userDB(req) only
9
+ * PublicXxxController → publicDB() only
10
+ * SystemXxxController → systemDB(context) only
11
+ * SuperadminXxxController → superadminDB(req) only
12
+ * WebhookXxxController → systemDB(context) only (webhooks have no user session)
13
+ *
14
+ * Violations:
15
+ * - CRITICAL: Controller uses systemDB when user context is available (should be adminDB/userDB)
16
+ * - HIGH: Controller mixes multiple DB helper types
17
+ * - MEDIUM: Controller DB helper doesn't match naming convention
18
+ *
19
+ * Reference: stella_howto_get slug="tetra-architecture-guide"
20
+ */
21
+
22
+ import { readFileSync, existsSync } from 'fs'
23
+ import { join, basename, dirname } from 'path'
24
+ import { glob } from 'glob'
25
+
26
+ const DB_PATTERNS = {
27
+ systemDB: { level: 'SYSTEM', pattern: /systemDB\s*\(/g, desc: 'System-level (cron, webhooks)' },
28
+ adminDB: { level: 'ADMIN', pattern: /adminDB\s*\(/g, desc: 'Admin operations (org-scoped)' },
29
+ userDB: { level: 'USER', pattern: /userDB\s*\(/g, desc: 'User-specific operations' },
30
+ publicDB: { level: 'PUBLIC', pattern: /publicDB\s*\(/g, desc: 'Public/unauthenticated' },
31
+ superadminDB: { level: 'SUPERADMIN', pattern: /superadminDB\s*\(/g, desc: 'Cross-org superadmin' }
32
+ }
33
+
34
+ /**
35
+ * Determine expected DB level from controller filename
36
+ */
37
+ function getExpectedLevel(fileName) {
38
+ const lower = fileName.toLowerCase()
39
+ // Order matters — check compound names first
40
+ if (lower.includes('system')) return 'SYSTEM'
41
+ if (lower.includes('superadmin')) return 'SUPERADMIN'
42
+ if (lower.includes('webhook')) return 'SYSTEM' // webhooks have no user session
43
+ if (lower.includes('cron')) return 'SYSTEM'
44
+ if (lower.includes('admin')) return 'ADMIN'
45
+ if (lower.includes('user')) return 'USER'
46
+ if (lower.includes('public')) return 'PUBLIC'
47
+ return null
48
+ }
49
+
50
+ /**
51
+ * Check if file has user authentication context available
52
+ */
53
+ function hasUserContext(content) {
54
+ return content.includes('AuthenticatedRequest') ||
55
+ content.includes('req.userToken') ||
56
+ content.includes('req.user') ||
57
+ content.includes('authenticateToken')
58
+ }
59
+
60
+ export async function check(projectRoot, config = {}) {
61
+ const results = {
62
+ name: 'Mixed DB Usage',
63
+ slug: 'mixed-db-usage',
64
+ passed: true,
65
+ skipped: false,
66
+ findings: [],
67
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
68
+ details: { controllersChecked: 0, violations: 0 }
69
+ }
70
+
71
+ const files = await glob('**/*[Cc]ontroller*.ts', {
72
+ cwd: projectRoot,
73
+ ignore: [
74
+ '**/node_modules/**',
75
+ '**/.next/**',
76
+ '**/dist/**',
77
+ '**/build/**',
78
+ ...(config.ignore || []),
79
+ '**/*.test.ts',
80
+ '**/*.spec.ts',
81
+ '**/*.d.ts'
82
+ ]
83
+ })
84
+
85
+ if (files.length === 0) {
86
+ results.skipped = true
87
+ results.skipReason = 'No controller files found'
88
+ return results
89
+ }
90
+
91
+ for (const file of files) {
92
+ const fullPath = join(projectRoot, file)
93
+ let content
94
+ try { content = readFileSync(fullPath, 'utf-8') } catch { continue }
95
+
96
+ results.details.controllersChecked++
97
+ const fileName = basename(file)
98
+
99
+ // Detect all DB usages in this file
100
+ const dbUsages = new Map() // level -> [{type, line, code}]
101
+ const lines = content.split('\n')
102
+
103
+ for (const [dbType, cfg] of Object.entries(DB_PATTERNS)) {
104
+ lines.forEach((line, idx) => {
105
+ // Reset regex lastIndex
106
+ cfg.pattern.lastIndex = 0
107
+ if (cfg.pattern.test(line)) {
108
+ const level = cfg.level
109
+ if (!dbUsages.has(level)) dbUsages.set(level, [])
110
+ dbUsages.get(level).push({
111
+ type: dbType,
112
+ line: idx + 1,
113
+ code: line.trim().substring(0, 120)
114
+ })
115
+ }
116
+ })
117
+ }
118
+
119
+ if (dbUsages.size === 0) continue
120
+
121
+ const expectedLevel = getExpectedLevel(fileName)
122
+ const usedLevels = [...dbUsages.keys()]
123
+ const userContextAvailable = hasUserContext(content)
124
+
125
+ // CRITICAL: systemDB used when user context is available and it's not a system controller
126
+ if (dbUsages.has('SYSTEM') && userContextAvailable && expectedLevel !== 'SYSTEM') {
127
+ results.passed = false
128
+ const locs = dbUsages.get('SYSTEM')
129
+ for (const loc of locs) {
130
+ results.findings.push({
131
+ file,
132
+ line: loc.line,
133
+ type: 'systemDB with user context',
134
+ severity: 'critical',
135
+ message: `${fileName} uses systemDB() but has user authentication context → MUST use ${expectedLevel === 'ADMIN' ? 'adminDB(req)' : expectedLevel === 'USER' ? 'userDB(req)' : 'adminDB(req) or userDB(req)'}`,
136
+ snippet: loc.code,
137
+ fix: `Replace systemDB('context') with ${expectedLevel === 'ADMIN' ? 'adminDB(req)' : 'userDB(req)'}. systemDB bypasses RLS — user operations MUST go through RLS. See: stella_howto_get slug="tetra-architecture-guide"`
138
+ })
139
+ results.summary.critical++
140
+ results.summary.total++
141
+ }
142
+ }
143
+
144
+ // HIGH: Multiple DB helper types in one controller
145
+ if (usedLevels.length > 1) {
146
+ results.passed = false
147
+ results.findings.push({
148
+ file,
149
+ line: 1,
150
+ type: 'mixed db usage',
151
+ severity: 'high',
152
+ message: `${fileName} mixes ${usedLevels.join(' + ')} database helpers. Controllers MUST use exactly ONE DB helper type.`,
153
+ fix: `Split into separate controllers per DB level, or refactor services to accept injected supabase client. Admin + System mix → move system ops to a separate SystemXxxController.`
154
+ })
155
+ results.summary.high++
156
+ results.summary.total++
157
+ }
158
+
159
+ // MEDIUM: DB helper doesn't match naming convention
160
+ if (expectedLevel && usedLevels.length === 1 && !usedLevels.includes(expectedLevel)) {
161
+ const actualLevel = usedLevels[0]
162
+ // Don't double-report if already caught as critical
163
+ if (!(actualLevel === 'SYSTEM' && userContextAvailable)) {
164
+ results.findings.push({
165
+ file,
166
+ line: 1,
167
+ type: 'naming mismatch',
168
+ severity: 'medium',
169
+ message: `${fileName} implies ${expectedLevel} but uses ${actualLevel}. Either rename the controller or change the DB helper.`,
170
+ fix: `Rename to match DB level, or change DB helper to match controller naming convention.`
171
+ })
172
+ results.summary.medium++
173
+ results.summary.total++
174
+ }
175
+ }
176
+ }
177
+
178
+ results.details.violations = results.findings.length
179
+ return results
180
+ }
@@ -106,39 +106,55 @@ export async function run(config, projectRoot) {
106
106
  }
107
107
  }
108
108
 
109
- if (!systemDbPath) {
109
+ // Also check if project has tetra-core as npm package (no local systemDb.ts)
110
+ let hasTetraCore = false
111
+ for (const pkgPath of [`${projectRoot}/package.json`, `${projectRoot}/backend/package.json`]) {
112
+ if (existsSync(pkgPath)) {
113
+ try {
114
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
115
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
116
+ if (deps['@soulbatical/tetra-core']) { hasTetraCore = true; break }
117
+ } catch { /* skip */ }
118
+ }
119
+ }
120
+
121
+ if (!systemDbPath && !hasTetraCore) {
110
122
  results.skipped = true
111
- results.skipReason = 'No systemDb.ts found'
123
+ results.skipReason = 'No systemDb.ts found and no @soulbatical/tetra-core dependency'
112
124
  return results
113
125
  }
114
126
 
115
- const systemDbContent = readFileSync(systemDbPath, 'utf-8')
116
- const whitelistMatch = systemDbContent.match(/new Set\s*(?:<[^>]+>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
117
-
118
127
  let whitelist = new Set()
119
- if (whitelistMatch) {
120
- const entries = whitelistMatch[1]
121
- .split('\n')
122
- .map(line => {
123
- const m = line.match(/['"]([^'"]+)['"]/)
124
- return m ? m[1] : null
125
- })
126
- .filter(Boolean)
127
- whitelist = new Set(entries)
128
- }
129
128
 
130
- if (whitelist.size > MAX_WHITELIST_SIZE) {
131
- results.passed = false
132
- results.findings.push({
133
- file: systemDbPath.replace(projectRoot + '/', ''),
134
- line: 1,
135
- type: 'whitelist-too-large',
136
- severity: 'critical',
137
- message: `systemDB whitelist has ${whitelist.size} entries (max: ${MAX_WHITELIST_SIZE}). This indicates systemDB is being used where SupabaseUserClient should be used instead. Refactor services to accept a supabase client parameter instead of calling systemDB() directly.`,
138
- fix: `Refactor: services should receive a supabase client via dependency injection, not create their own via systemDB(). Only cron jobs, webhooks, and OAuth callbacks should use systemDB.`
139
- })
140
- results.summary.critical++
141
- results.summary.total++
129
+ // Only check whitelist size if there's a local systemDb.ts
130
+ if (systemDbPath) {
131
+ const systemDbContent = readFileSync(systemDbPath, 'utf-8')
132
+ const whitelistMatch = systemDbContent.match(/new Set\s*(?:<[^>]+>)?\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
133
+
134
+ if (whitelistMatch) {
135
+ const entries = whitelistMatch[1]
136
+ .split('\n')
137
+ .map(line => {
138
+ const m = line.match(/['"]([^'"]+)['"]/)
139
+ return m ? m[1] : null
140
+ })
141
+ .filter(Boolean)
142
+ whitelist = new Set(entries)
143
+ }
144
+
145
+ if (whitelist.size > MAX_WHITELIST_SIZE) {
146
+ results.passed = false
147
+ results.findings.push({
148
+ file: systemDbPath.replace(projectRoot + '/', ''),
149
+ line: 1,
150
+ type: 'whitelist-too-large',
151
+ severity: 'critical',
152
+ message: `systemDB whitelist has ${whitelist.size} entries (max: ${MAX_WHITELIST_SIZE}). This indicates systemDB is being used where adminDB/userDB should be used instead.`,
153
+ fix: `Refactor: services should receive a supabase client via dependency injection, not create their own via systemDB(). Only cron jobs, webhooks, and OAuth callbacks should use systemDB.`
154
+ })
155
+ results.summary.critical++
156
+ results.summary.total++
157
+ }
142
158
  }
143
159
 
144
160
  // ─── Check 2: systemDB in forbidden locations ───────────────
@@ -146,6 +162,10 @@ export async function run(config, projectRoot) {
146
162
  const files = await glob('**/*.ts', {
147
163
  cwd: projectRoot,
148
164
  ignore: [
165
+ '**/node_modules/**',
166
+ '**/.next/**',
167
+ '**/dist/**',
168
+ '**/build/**',
149
169
  ...config.ignore,
150
170
  '**/systemDb.ts',
151
171
  '**/superadminDb.ts',
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Tetra Core Compliance — HARD BLOCK
3
+ *
4
+ * If a project has @soulbatical/tetra-core as a dependency, it MUST use:
5
+ * 1. configureAuth() — sets up authentication middleware
6
+ * 2. authenticateToken — middleware on protected routes
7
+ * 3. adminDB/userDB/systemDB — db helpers (NOT raw createClient)
8
+ *
9
+ * If ANY of these are missing, the project is NOT secure.
10
+ * There is no "skip" — if you have tetra-core, you use it fully or not at all.
11
+ *
12
+ * This check also detects projects that SHOULD have tetra-core but don't:
13
+ * - Has a backend/ dir with Express + Supabase → MUST have tetra-core
14
+ */
15
+
16
+ import { readFileSync, existsSync } from 'fs'
17
+ import { join } from 'path'
18
+ import { glob } from 'glob'
19
+
20
+ export const meta = {
21
+ id: 'tetra-core-compliance',
22
+ name: 'Tetra Core Compliance',
23
+ category: 'security',
24
+ severity: 'critical',
25
+ description: 'Ensures projects using @soulbatical/tetra-core implement ALL required security patterns — auth, middleware, db helpers'
26
+ }
27
+
28
+ export async function run(config, projectRoot) {
29
+ const results = {
30
+ passed: true,
31
+ findings: [],
32
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
33
+ }
34
+
35
+ // ─── Step 1: Does this project have tetra-core? ────────────────
36
+
37
+ const packageJsonPaths = [
38
+ join(projectRoot, 'package.json'),
39
+ join(projectRoot, 'backend', 'package.json')
40
+ ]
41
+
42
+ let hasTetraCore = false
43
+ let tetraCoreLocation = null
44
+
45
+ for (const pkgPath of packageJsonPaths) {
46
+ if (!existsSync(pkgPath)) continue
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
49
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
50
+ if (deps['@soulbatical/tetra-core']) {
51
+ hasTetraCore = true
52
+ tetraCoreLocation = pkgPath.replace(projectRoot + '/', '')
53
+ break
54
+ }
55
+ } catch { /* skip */ }
56
+ }
57
+
58
+ // ─── Step 2: Check if project SHOULD have tetra-core ───────────
59
+
60
+ if (!hasTetraCore) {
61
+ // Check if this is an Express + Supabase backend that should have it
62
+ const backendDirs = ['backend/src', 'src']
63
+ let hasExpress = false
64
+ let hasSupabase = false
65
+
66
+ for (const dir of backendDirs) {
67
+ const pkgPath = dir === 'src' ? join(projectRoot, 'package.json') : join(projectRoot, 'backend', 'package.json')
68
+ if (!existsSync(pkgPath)) continue
69
+ try {
70
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
71
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
72
+ if (deps['express']) hasExpress = true
73
+ if (deps['@supabase/supabase-js']) hasSupabase = true
74
+ } catch { /* skip */ }
75
+ }
76
+
77
+ if (hasExpress && hasSupabase) {
78
+ results.passed = false
79
+ results.findings.push({
80
+ type: 'missing-tetra-core',
81
+ severity: 'critical',
82
+ message: 'Project has Express + Supabase but does NOT have @soulbatical/tetra-core. This means NO standard auth middleware, NO db helpers, NO RLS enforcement. Install tetra-core and follow the architecture guide.',
83
+ fix: 'npm install @soulbatical/tetra-core && see stella_howto_get slug="tetra-architecture-guide"'
84
+ })
85
+ results.summary.critical++
86
+ results.summary.total++
87
+ } else {
88
+ // Not a backend project, skip
89
+ results.skipped = true
90
+ results.skipReason = 'Not a Tetra backend project (no Express + Supabase)'
91
+ return results
92
+ }
93
+
94
+ return results
95
+ }
96
+
97
+ // ─── Step 3: Has tetra-core → check FULL compliance ────────────
98
+
99
+ const backendSrc = existsSync(join(projectRoot, 'backend', 'src')) ? 'backend/src' : 'src'
100
+
101
+ // Check 3a: configureAuth() is called somewhere
102
+ const tsFiles = await glob(`${backendSrc}/**/*.ts`, {
103
+ cwd: projectRoot,
104
+ ignore: ['**/node_modules/**', '**/*.test.ts', '**/*.spec.ts', '**/*.d.ts']
105
+ })
106
+
107
+ let hasConfigureAuth = false
108
+ let hasAuthenticateToken = false
109
+ let hasDbHelpers = false
110
+ let dbHelperImports = { adminDB: false, userDB: false, systemDB: false, publicDB: false, superadminDB: false }
111
+ let hasRawCreateClient = false
112
+ let rawCreateClientFiles = []
113
+
114
+ for (const file of tsFiles) {
115
+ try {
116
+ const content = readFileSync(join(projectRoot, file), 'utf-8')
117
+
118
+ if (content.includes('configureAuth')) hasConfigureAuth = true
119
+ if (content.includes('authenticateToken')) hasAuthenticateToken = true
120
+
121
+ // Check for db helper usage
122
+ if (/(?:import|from).*(?:adminDB|userDB|systemDB|publicDB|superadminDB)/.test(content)) {
123
+ hasDbHelpers = true
124
+ if (content.includes('adminDB')) dbHelperImports.adminDB = true
125
+ if (content.includes('userDB')) dbHelperImports.userDB = true
126
+ if (content.includes('systemDB')) dbHelperImports.systemDB = true
127
+ if (content.includes('publicDB')) dbHelperImports.publicDB = true
128
+ if (content.includes('superadminDB')) dbHelperImports.superadminDB = true
129
+ }
130
+
131
+ // Check for raw createClient usage (should be caught by direct-supabase-client check too)
132
+ if (/import\s*\{[^}]*createClient[^}]*\}\s*from\s*['"]@supabase\/supabase-js['"]/.test(content)) {
133
+ if (!/import\s+type/.test(content)) {
134
+ // Not in core wrapper files
135
+ if (!/core\//.test(file) && !/SupabaseUserClient/.test(file) && !/scripts\//.test(file)) {
136
+ hasRawCreateClient = true
137
+ rawCreateClientFiles.push(file)
138
+ }
139
+ }
140
+ }
141
+ } catch { /* skip */ }
142
+ }
143
+
144
+ // ─── Report findings ──────────────────────────────────────────
145
+
146
+ if (!hasConfigureAuth) {
147
+ results.passed = false
148
+ results.findings.push({
149
+ type: 'missing-configureAuth',
150
+ severity: 'critical',
151
+ message: 'Project has @soulbatical/tetra-core but NEVER calls configureAuth(). This means the auth middleware is not initialized — all routes are potentially unprotected.',
152
+ fix: 'Create an auth-config.ts that calls configureAuth({ loadUserContext, roleHierarchy }). See sparkbuddy-live/backend/src/auth-config.ts for reference.'
153
+ })
154
+ results.summary.critical++
155
+ results.summary.total++
156
+ }
157
+
158
+ if (!hasAuthenticateToken) {
159
+ results.passed = false
160
+ results.findings.push({
161
+ type: 'missing-authenticateToken',
162
+ severity: 'critical',
163
+ message: 'Project has @soulbatical/tetra-core but NO route uses authenticateToken middleware. All API endpoints are unprotected.',
164
+ fix: 'Add authenticateToken middleware to all protected routes: router.use(authenticateToken, requireOrganizationAdmin)'
165
+ })
166
+ results.summary.critical++
167
+ results.summary.total++
168
+ }
169
+
170
+ if (!hasDbHelpers) {
171
+ results.passed = false
172
+ results.findings.push({
173
+ type: 'missing-db-helpers',
174
+ severity: 'critical',
175
+ message: 'Project has @soulbatical/tetra-core but does NOT use any db helpers (adminDB, userDB, systemDB, publicDB). Database access has no RLS enforcement.',
176
+ fix: 'Use adminDB(req) for admin routes, userDB(req) for user routes, systemDB(context) for system operations. See stella_howto_get slug="tetra-architecture-guide"'
177
+ })
178
+ results.summary.critical++
179
+ results.summary.total++
180
+ }
181
+
182
+ // Info: which helpers are used
183
+ const usedHelpers = Object.entries(dbHelperImports).filter(([_, used]) => used).map(([name]) => name)
184
+ const unusedHelpers = Object.entries(dbHelperImports).filter(([_, used]) => !used).map(([name]) => name)
185
+
186
+ results.info = {
187
+ tetraCoreLocation,
188
+ configureAuth: hasConfigureAuth,
189
+ authenticateToken: hasAuthenticateToken,
190
+ dbHelpers: {
191
+ used: usedHelpers,
192
+ unused: unusedHelpers
193
+ }
194
+ }
195
+
196
+ return results
197
+ }
package/lib/runner.js CHANGED
@@ -12,6 +12,8 @@ import * as serviceKeyExposure from './checks/security/service-key-exposure.js'
12
12
  import * as deprecatedSupabaseAdmin from './checks/security/deprecated-supabase-admin.js'
13
13
  import * as directSupabaseClient from './checks/security/direct-supabase-client.js'
14
14
  import * as frontendSupabaseQueries from './checks/security/frontend-supabase-queries.js'
15
+ import * as tetraCoreCompliance from './checks/security/tetra-core-compliance.js'
16
+ import * as mixedDbUsage from './checks/security/mixed-db-usage.js'
15
17
  import * as systemdbWhitelist from './checks/security/systemdb-whitelist.js'
16
18
  import * as huskyHooks from './checks/stability/husky-hooks.js'
17
19
  import * as ciPipeline from './checks/stability/ci-pipeline.js'
@@ -35,6 +37,8 @@ const ALL_CHECKS = {
35
37
  deprecatedSupabaseAdmin,
36
38
  directSupabaseClient,
37
39
  frontendSupabaseQueries,
40
+ tetraCoreCompliance,
41
+ mixedDbUsage,
38
42
  systemdbWhitelist,
39
43
  gitignoreValidation
40
44
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.10.2",
3
+ "version": "1.12.0",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },