@girardmedia/bootspring 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/bootspring.js +35 -96
- package/claude-commands/agent.md +34 -0
- package/claude-commands/bs.md +31 -0
- package/claude-commands/build.md +25 -0
- package/claude-commands/skill.md +31 -0
- package/claude-commands/todo.md +25 -0
- package/dist/cli/index.cjs +17808 -0
- package/dist/core/index.d.ts +5814 -0
- package/dist/core.js +5780 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp-server.js +2299 -0
- package/generators/api-docs.js +2 -2
- package/generators/decisions.js +3 -3
- package/generators/health.js +16 -16
- package/generators/sprint.js +2 -2
- package/package.json +27 -59
- package/core/api-client.d.ts +0 -69
- package/core/api-client.js +0 -1482
- package/core/auth.d.ts +0 -98
- package/core/auth.js +0 -737
- package/core/build-orchestrator.js +0 -508
- package/core/build-state.js +0 -612
- package/core/config.d.ts +0 -106
- package/core/config.js +0 -1328
- package/core/context-loader.js +0 -580
- package/core/context.d.ts +0 -61
- package/core/context.js +0 -327
- package/core/entitlements.d.ts +0 -70
- package/core/entitlements.js +0 -322
- package/core/index.d.ts +0 -53
- package/core/index.js +0 -62
- package/core/mcp-config.js +0 -115
- package/core/policies.d.ts +0 -43
- package/core/policies.js +0 -113
- package/core/policy-matrix.js +0 -303
- package/core/project-activity.js +0 -175
- package/core/redaction.d.ts +0 -5
- package/core/redaction.js +0 -63
- package/core/self-update.js +0 -259
- package/core/session.js +0 -353
- package/core/task-extractor.js +0 -1098
- package/core/telemetry.d.ts +0 -55
- package/core/telemetry.js +0 -617
- package/core/tier-enforcement.js +0 -928
- package/core/utils.d.ts +0 -90
- package/core/utils.js +0 -455
- package/core/validation.js +0 -572
- package/mcp/server.d.ts +0 -57
- package/mcp/server.js +0 -264
package/core/context.js
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring Context Manager
|
|
3
|
-
* Handles project context for AI assistants
|
|
4
|
-
*
|
|
5
|
-
* @package bootspring
|
|
6
|
-
* @module core/context
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const config = require('./config');
|
|
11
|
-
const utils = require('./utils');
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Get current project context
|
|
15
|
-
* @param {object} [options] - Options
|
|
16
|
-
* @returns {object} Context object
|
|
17
|
-
*/
|
|
18
|
-
function get(options = {}) {
|
|
19
|
-
const cfg = options.config || config.load();
|
|
20
|
-
const projectRoot = cfg._projectRoot;
|
|
21
|
-
|
|
22
|
-
const context = {
|
|
23
|
-
project: cfg.project,
|
|
24
|
-
stack: cfg.stack,
|
|
25
|
-
plugins: getEnabledPlugins(cfg),
|
|
26
|
-
files: getProjectFiles(projectRoot),
|
|
27
|
-
git: getGitInfo(projectRoot),
|
|
28
|
-
state: getProjectState(projectRoot, cfg),
|
|
29
|
-
timestamp: new Date().toISOString()
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return context;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Get enabled plugins from config
|
|
37
|
-
* @param {object} cfg - Configuration
|
|
38
|
-
* @returns {object} Enabled plugins
|
|
39
|
-
*/
|
|
40
|
-
function getEnabledPlugins(cfg) {
|
|
41
|
-
const enabled = {};
|
|
42
|
-
|
|
43
|
-
for (const [name, plugin] of Object.entries(cfg.plugins || {})) {
|
|
44
|
-
if (plugin.enabled !== false) {
|
|
45
|
-
enabled[name] = {
|
|
46
|
-
provider: plugin.provider || 'default',
|
|
47
|
-
features: plugin.features || []
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return enabled;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Get project file structure summary
|
|
57
|
-
* @param {string} projectRoot - Project root
|
|
58
|
-
* @returns {object} File info
|
|
59
|
-
*/
|
|
60
|
-
function getProjectFiles(projectRoot) {
|
|
61
|
-
const files = {
|
|
62
|
-
hasPackageJson: utils.fileExists(path.join(projectRoot, 'package.json')),
|
|
63
|
-
hasTsConfig: utils.fileExists(path.join(projectRoot, 'tsconfig.json')),
|
|
64
|
-
hasClaudeMd: utils.fileExists(path.join(projectRoot, 'CLAUDE.md')),
|
|
65
|
-
hasBootspringConfig: utils.fileExists(path.join(projectRoot, 'bootspring.config.js')),
|
|
66
|
-
hasTodoMd: utils.fileExists(path.join(projectRoot, 'todo.md')),
|
|
67
|
-
hasGit: utils.fileExists(path.join(projectRoot, '.git')),
|
|
68
|
-
hasSrcDir: utils.fileExists(path.join(projectRoot, 'src')),
|
|
69
|
-
hasAppDir: utils.fileExists(path.join(projectRoot, 'app')),
|
|
70
|
-
hasPagesDir: utils.fileExists(path.join(projectRoot, 'pages'))
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Detect framework structure
|
|
74
|
-
if (files.hasAppDir) {
|
|
75
|
-
files.structure = 'app-router';
|
|
76
|
-
} else if (files.hasPagesDir) {
|
|
77
|
-
files.structure = 'pages-router';
|
|
78
|
-
} else if (files.hasSrcDir) {
|
|
79
|
-
files.structure = 'src-based';
|
|
80
|
-
} else {
|
|
81
|
-
files.structure = 'flat';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return files;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Get git information
|
|
89
|
-
* @param {string} projectRoot - Project root
|
|
90
|
-
* @returns {object} Git info
|
|
91
|
-
*/
|
|
92
|
-
function getGitInfo(projectRoot) {
|
|
93
|
-
const gitDir = path.join(projectRoot, '.git');
|
|
94
|
-
|
|
95
|
-
if (!utils.fileExists(gitDir)) {
|
|
96
|
-
return { initialized: false };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const info = { initialized: true };
|
|
100
|
-
|
|
101
|
-
// Get current branch
|
|
102
|
-
const headPath = path.join(gitDir, 'HEAD');
|
|
103
|
-
if (utils.fileExists(headPath)) {
|
|
104
|
-
const head = utils.readFile(headPath).trim();
|
|
105
|
-
if (head.startsWith('ref: refs/heads/')) {
|
|
106
|
-
info.branch = head.replace('ref: refs/heads/', '');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Check for remote
|
|
111
|
-
const configPath = path.join(gitDir, 'config');
|
|
112
|
-
if (utils.fileExists(configPath)) {
|
|
113
|
-
const gitConfig = utils.readFile(configPath);
|
|
114
|
-
info.hasRemote = gitConfig.includes('[remote "origin"]');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return info;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Get project state
|
|
122
|
-
* @param {string} projectRoot - Project root
|
|
123
|
-
* @param {object} cfg - Configuration
|
|
124
|
-
* @returns {object} State info
|
|
125
|
-
*/
|
|
126
|
-
function getProjectState(projectRoot, cfg) {
|
|
127
|
-
const state = {
|
|
128
|
-
phase: 'unknown',
|
|
129
|
-
health: 'unknown',
|
|
130
|
-
todos: 0,
|
|
131
|
-
lastGenerated: null
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Count todos
|
|
135
|
-
const todoPath = path.join(projectRoot, cfg.paths?.todo || 'todo.md');
|
|
136
|
-
if (utils.fileExists(todoPath)) {
|
|
137
|
-
const content = utils.readFile(todoPath);
|
|
138
|
-
const todoMatches = content.match(/- \[ \]/g);
|
|
139
|
-
state.todos = todoMatches ? todoMatches.length : 0;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Check CLAUDE.md generation time
|
|
143
|
-
const claudePath = path.join(projectRoot, cfg.paths?.context || 'CLAUDE.md');
|
|
144
|
-
if (utils.fileExists(claudePath)) {
|
|
145
|
-
state.lastGenerated = utils.getFileTime(claudePath);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Determine phase
|
|
149
|
-
if (!cfg._configPath) {
|
|
150
|
-
state.phase = 'uninitialized';
|
|
151
|
-
} else if (!state.lastGenerated) {
|
|
152
|
-
state.phase = 'initialized';
|
|
153
|
-
} else {
|
|
154
|
-
state.phase = 'active';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Determine health
|
|
158
|
-
const issues = [];
|
|
159
|
-
if (!utils.fileExists(path.join(projectRoot, 'package.json'))) {
|
|
160
|
-
issues.push('missing-package-json');
|
|
161
|
-
}
|
|
162
|
-
if (!cfg._configPath) {
|
|
163
|
-
issues.push('missing-config');
|
|
164
|
-
}
|
|
165
|
-
if (!state.lastGenerated) {
|
|
166
|
-
issues.push('missing-context');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (issues.length === 0) {
|
|
170
|
-
state.health = 'good';
|
|
171
|
-
} else if (issues.length <= 2) {
|
|
172
|
-
state.health = 'fair';
|
|
173
|
-
} else {
|
|
174
|
-
state.health = 'needs-attention';
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
state.issues = issues;
|
|
178
|
-
|
|
179
|
-
return state;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Validate project context
|
|
184
|
-
* @param {object} [options] - Options
|
|
185
|
-
* @returns {object} Validation result
|
|
186
|
-
*/
|
|
187
|
-
function validate(options = {}) {
|
|
188
|
-
const cfg = options.config || config.load();
|
|
189
|
-
const projectRoot = cfg._projectRoot;
|
|
190
|
-
|
|
191
|
-
const checks = [];
|
|
192
|
-
let score = 0;
|
|
193
|
-
const maxScore = 10;
|
|
194
|
-
|
|
195
|
-
// Check 1: Config exists
|
|
196
|
-
if (cfg._configPath) {
|
|
197
|
-
checks.push({ name: 'Configuration', status: 'pass', message: 'bootspring.config.js found' });
|
|
198
|
-
score += 2;
|
|
199
|
-
} else {
|
|
200
|
-
checks.push({ name: 'Configuration', status: 'fail', message: 'bootspring.config.js missing' });
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Check 2: CLAUDE.md exists
|
|
204
|
-
const claudePath = path.join(projectRoot, cfg.paths?.context || 'CLAUDE.md');
|
|
205
|
-
if (utils.fileExists(claudePath)) {
|
|
206
|
-
checks.push({ name: 'AI Context', status: 'pass', message: 'CLAUDE.md exists' });
|
|
207
|
-
score += 2;
|
|
208
|
-
} else {
|
|
209
|
-
checks.push({ name: 'AI Context', status: 'fail', message: 'CLAUDE.md missing - run bootspring generate' });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Check 3: package.json exists
|
|
213
|
-
if (utils.fileExists(path.join(projectRoot, 'package.json'))) {
|
|
214
|
-
checks.push({ name: 'Package', status: 'pass', message: 'package.json found' });
|
|
215
|
-
score += 1;
|
|
216
|
-
} else {
|
|
217
|
-
checks.push({ name: 'Package', status: 'warn', message: 'package.json missing' });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check 4: Git initialized
|
|
221
|
-
if (utils.fileExists(path.join(projectRoot, '.git'))) {
|
|
222
|
-
checks.push({ name: 'Git', status: 'pass', message: 'Git repository initialized' });
|
|
223
|
-
score += 1;
|
|
224
|
-
} else {
|
|
225
|
-
checks.push({ name: 'Git', status: 'warn', message: 'Git not initialized' });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Check 5: TypeScript config (if using TS)
|
|
229
|
-
if (cfg.stack?.language === 'typescript') {
|
|
230
|
-
if (utils.fileExists(path.join(projectRoot, 'tsconfig.json'))) {
|
|
231
|
-
checks.push({ name: 'TypeScript', status: 'pass', message: 'tsconfig.json found' });
|
|
232
|
-
score += 1;
|
|
233
|
-
} else {
|
|
234
|
-
checks.push({ name: 'TypeScript', status: 'fail', message: 'tsconfig.json missing for TypeScript project' });
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
score += 1;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Check 6: Config validation
|
|
241
|
-
const configValidation = config.validate(cfg);
|
|
242
|
-
if (configValidation.valid) {
|
|
243
|
-
checks.push({ name: 'Config Validation', status: 'pass', message: 'Configuration is valid' });
|
|
244
|
-
score += 2;
|
|
245
|
-
} else {
|
|
246
|
-
checks.push({ name: 'Config Validation', status: 'fail', message: configValidation.errors.join(', ') });
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Check 7: Todo file
|
|
250
|
-
if (utils.fileExists(path.join(projectRoot, cfg.paths?.todo || 'todo.md'))) {
|
|
251
|
-
checks.push({ name: 'Todo Tracking', status: 'pass', message: 'todo.md exists' });
|
|
252
|
-
score += 1;
|
|
253
|
-
} else {
|
|
254
|
-
checks.push({ name: 'Todo Tracking', status: 'fail', message: 'todo.md not found' });
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
valid: score >= maxScore * 0.6,
|
|
259
|
-
score,
|
|
260
|
-
maxScore,
|
|
261
|
-
percentage: Math.round((score / maxScore) * 100),
|
|
262
|
-
checks
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Generate context summary for AI
|
|
268
|
-
* @param {object} [options] - Options
|
|
269
|
-
* @returns {string} Context summary markdown
|
|
270
|
-
*/
|
|
271
|
-
function generateSummary(options = {}) {
|
|
272
|
-
const ctx = get(options);
|
|
273
|
-
|
|
274
|
-
const lines = [
|
|
275
|
-
'# Project Context',
|
|
276
|
-
'',
|
|
277
|
-
`**Project**: ${ctx.project.name}`,
|
|
278
|
-
`**Generated**: ${ctx.timestamp}`,
|
|
279
|
-
'',
|
|
280
|
-
'## Stack',
|
|
281
|
-
`- Framework: ${ctx.stack.framework}`,
|
|
282
|
-
`- Language: ${ctx.stack.language}`,
|
|
283
|
-
`- Database: ${ctx.stack.database}`,
|
|
284
|
-
`- Hosting: ${ctx.stack.hosting}`,
|
|
285
|
-
''
|
|
286
|
-
];
|
|
287
|
-
|
|
288
|
-
// Plugins
|
|
289
|
-
const enabledPlugins = Object.keys(ctx.plugins);
|
|
290
|
-
if (enabledPlugins.length > 0) {
|
|
291
|
-
lines.push('## Enabled Plugins');
|
|
292
|
-
for (const [name, plugin] of Object.entries(ctx.plugins)) {
|
|
293
|
-
lines.push(`- **${name}**: ${plugin.provider}`);
|
|
294
|
-
}
|
|
295
|
-
lines.push('');
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// State
|
|
299
|
-
lines.push('## Project State');
|
|
300
|
-
lines.push(`- Phase: ${ctx.state.phase}`);
|
|
301
|
-
lines.push(`- Health: ${ctx.state.health}`);
|
|
302
|
-
lines.push(`- Open Todos: ${ctx.state.todos}`);
|
|
303
|
-
if (ctx.state.lastGenerated) {
|
|
304
|
-
lines.push(`- Context Last Generated: ${utils.formatRelativeTime(ctx.state.lastGenerated)}`);
|
|
305
|
-
}
|
|
306
|
-
lines.push('');
|
|
307
|
-
|
|
308
|
-
// Git
|
|
309
|
-
if (ctx.git.initialized) {
|
|
310
|
-
lines.push('## Git');
|
|
311
|
-
lines.push(`- Branch: ${ctx.git.branch || 'unknown'}`);
|
|
312
|
-
lines.push(`- Remote: ${ctx.git.hasRemote ? 'configured' : 'not configured'}`);
|
|
313
|
-
lines.push('');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return lines.join('\n');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
module.exports = {
|
|
320
|
-
get,
|
|
321
|
-
validate,
|
|
322
|
-
generateSummary,
|
|
323
|
-
getEnabledPlugins,
|
|
324
|
-
getProjectFiles,
|
|
325
|
-
getGitInfo,
|
|
326
|
-
getProjectState
|
|
327
|
-
};
|
package/core/entitlements.d.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring Entitlements Types
|
|
3
|
-
* @module core/entitlements
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Tier } from './auth';
|
|
7
|
-
|
|
8
|
-
export interface AccessResult {
|
|
9
|
-
allowed: boolean;
|
|
10
|
-
reason?: string;
|
|
11
|
-
requiredTier?: Tier;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface SkillAccess extends AccessResult {
|
|
15
|
-
skillId: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface WorkflowAccess extends AccessResult {
|
|
19
|
-
workflowId: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check if user has access to a skill
|
|
24
|
-
* @param skillId - Skill identifier
|
|
25
|
-
* @param options - Check options
|
|
26
|
-
* @returns Access result
|
|
27
|
-
*/
|
|
28
|
-
export function checkSkillAccess(
|
|
29
|
-
skillId: string,
|
|
30
|
-
options?: { tier?: Tier; serverMode?: boolean }
|
|
31
|
-
): SkillAccess;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Check if user has access to a workflow
|
|
35
|
-
* @param workflowId - Workflow identifier
|
|
36
|
-
* @param options - Check options
|
|
37
|
-
* @returns Access result
|
|
38
|
-
*/
|
|
39
|
-
export function checkWorkflowAccess(
|
|
40
|
-
workflowId: string,
|
|
41
|
-
options?: { tier?: Tier; serverMode?: boolean }
|
|
42
|
-
): WorkflowAccess;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Filter skills by access
|
|
46
|
-
* @param skills - List of skill IDs
|
|
47
|
-
* @param options - Filter options
|
|
48
|
-
* @returns Filtered skills with access info
|
|
49
|
-
*/
|
|
50
|
-
export function filterSkillsByAccess(
|
|
51
|
-
skills: string[],
|
|
52
|
-
options?: { tier?: Tier }
|
|
53
|
-
): { allowed: string[]; denied: SkillAccess[] };
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Filter workflows by access
|
|
57
|
-
* @param workflows - List of workflow IDs
|
|
58
|
-
* @param options - Filter options
|
|
59
|
-
* @returns Filtered workflows with access info
|
|
60
|
-
*/
|
|
61
|
-
export function filterWorkflowsByAccess(
|
|
62
|
-
workflows: string[],
|
|
63
|
-
options?: { tier?: Tier }
|
|
64
|
-
): { allowed: string[]; denied: WorkflowAccess[] };
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Check if running in server mode
|
|
68
|
-
* @returns True if in server mode
|
|
69
|
-
*/
|
|
70
|
-
export function isServerMode(): boolean;
|
package/core/entitlements.js
DELETED
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring Entitlements
|
|
3
|
-
* Shared access policy for gated capabilities.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const LOCAL_MODE = 'local';
|
|
7
|
-
const SERVER_MODE = 'server';
|
|
8
|
-
const policies = require('./policies');
|
|
9
|
-
const KNOWN_TIERS = new Set(['free', 'founder', 'pro', 'team', 'enterprise', 'custom']);
|
|
10
|
-
|
|
11
|
-
// Use tier-enforcement for tier checking (lazy-loaded to avoid circular dependency)
|
|
12
|
-
let _tierEnforcement = null;
|
|
13
|
-
function getTierEnforcement() {
|
|
14
|
-
if (!_tierEnforcement) {
|
|
15
|
-
_tierEnforcement = require('./tier-enforcement');
|
|
16
|
-
}
|
|
17
|
-
return _tierEnforcement;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Check if tier meets pro requirement
|
|
21
|
-
function isProTier(tier) {
|
|
22
|
-
return getTierEnforcement().meetsTierRequirement('pro', tier);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function parseBoolean(value) {
|
|
26
|
-
if (typeof value === 'boolean') return value;
|
|
27
|
-
if (value === null || value === undefined) return false;
|
|
28
|
-
const normalized = String(value).trim().toLowerCase();
|
|
29
|
-
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeTier(value) {
|
|
33
|
-
const tier = String(value || 'free').trim().toLowerCase();
|
|
34
|
-
return KNOWN_TIERS.has(tier) ? tier : 'free';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function normalizeMode(value) {
|
|
38
|
-
const mode = String(value || '').trim().toLowerCase();
|
|
39
|
-
if (mode === SERVER_MODE) {
|
|
40
|
-
return SERVER_MODE;
|
|
41
|
-
}
|
|
42
|
-
return LOCAL_MODE;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function resolveAccessContext(options = {}) {
|
|
46
|
-
const envMode = process.env.BOOTSPRING_SKILL_ACCESS_MODE;
|
|
47
|
-
const envTier = process.env.BOOTSPRING_USER_TIER;
|
|
48
|
-
const envEntitled = process.env.BOOTSPRING_SKILLS_ENTITLED;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
mode: normalizeMode(options.mode || envMode),
|
|
52
|
-
tier: normalizeTier(options.tier || envTier || 'free'),
|
|
53
|
-
entitled: parseBoolean(options.entitled ?? envEntitled),
|
|
54
|
-
policyProfile: policies.resolvePolicyProfile(options)
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function resolveWorkflowAccessContext(options = {}) {
|
|
59
|
-
const envMode = process.env.BOOTSPRING_WORKFLOW_ACCESS_MODE || process.env.BOOTSPRING_SKILL_ACCESS_MODE;
|
|
60
|
-
const envTier = process.env.BOOTSPRING_USER_TIER;
|
|
61
|
-
const envEntitled = process.env.BOOTSPRING_WORKFLOWS_ENTITLED ?? process.env.BOOTSPRING_SKILLS_ENTITLED;
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
mode: normalizeMode(options.mode || envMode),
|
|
65
|
-
tier: normalizeTier(options.tier || envTier || 'free'),
|
|
66
|
-
entitled: parseBoolean(options.entitled ?? envEntitled),
|
|
67
|
-
policyProfile: policies.resolvePolicyProfile(options)
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function isExternalSkill(skillId) {
|
|
72
|
-
return String(skillId || '').trim().toLowerCase().startsWith('external/');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function isPremiumPattern(skillTier) {
|
|
76
|
-
const tier = String(skillTier || 'free').trim().toLowerCase();
|
|
77
|
-
return tier === 'pro' || tier === 'premium';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function checkSkillAccess(skillId, options = {}) {
|
|
81
|
-
const context = resolveAccessContext(options);
|
|
82
|
-
const skillTier = options.skillTier;
|
|
83
|
-
|
|
84
|
-
// External skills
|
|
85
|
-
if (isExternalSkill(skillId)) {
|
|
86
|
-
const policy = policies.getPolicyProfile(context.policyProfile, options);
|
|
87
|
-
if (!policy.allowExternalSkills) {
|
|
88
|
-
return {
|
|
89
|
-
allowed: false,
|
|
90
|
-
code: 'external_policy_blocked',
|
|
91
|
-
reason: `External skills are blocked by ${policy.id} policy profile.`,
|
|
92
|
-
context
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (context.mode !== SERVER_MODE) {
|
|
97
|
-
return {
|
|
98
|
-
allowed: true,
|
|
99
|
-
code: 'external_local_mode',
|
|
100
|
-
reason: 'External skills are enabled in local mode.',
|
|
101
|
-
context
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (context.entitled || isProTier(context.tier)) {
|
|
106
|
-
return {
|
|
107
|
-
allowed: true,
|
|
108
|
-
code: 'external_entitled',
|
|
109
|
-
reason: 'External skill access granted.',
|
|
110
|
-
context
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
allowed: false,
|
|
116
|
-
code: 'external_subscription_required',
|
|
117
|
-
reason: 'External skills require entitlement in server mode. Set BOOTSPRING_SKILLS_ENTITLED=true or use tier=pro/team/enterprise.',
|
|
118
|
-
context
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Local mode - all patterns accessible for development
|
|
123
|
-
if (context.mode !== SERVER_MODE) {
|
|
124
|
-
if (isPremiumPattern(skillTier)) {
|
|
125
|
-
return {
|
|
126
|
-
allowed: true,
|
|
127
|
-
code: 'premium_local_mode',
|
|
128
|
-
reason: 'Premium patterns are enabled in local mode.',
|
|
129
|
-
context
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
allowed: true,
|
|
134
|
-
code: 'free_local_mode',
|
|
135
|
-
reason: 'Patterns are enabled in local mode.',
|
|
136
|
-
context
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Server mode - all patterns require authentication
|
|
141
|
-
// Check if user has any entitlement (authenticated)
|
|
142
|
-
const isAuthenticated = context.entitled || context.tier !== 'free' || isProTier(context.tier);
|
|
143
|
-
|
|
144
|
-
if (!isAuthenticated) {
|
|
145
|
-
// Unauthenticated in server mode - block all patterns
|
|
146
|
-
return {
|
|
147
|
-
allowed: false,
|
|
148
|
-
code: 'authentication_required',
|
|
149
|
-
reason: 'Patterns require authentication in server mode. Sign in at bootspring.com or use BOOTSPRING_SKILLS_ENTITLED=true for development.',
|
|
150
|
-
context
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Authenticated - check tier for premium patterns
|
|
155
|
-
if (isPremiumPattern(skillTier)) {
|
|
156
|
-
if (isProTier(context.tier)) {
|
|
157
|
-
return {
|
|
158
|
-
allowed: true,
|
|
159
|
-
code: 'premium_entitled',
|
|
160
|
-
reason: 'Premium pattern access granted.',
|
|
161
|
-
context
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
allowed: false,
|
|
166
|
-
code: 'premium_subscription_required',
|
|
167
|
-
reason: 'Premium pattern requires Pro subscription. Upgrade at bootspring.com/pricing.',
|
|
168
|
-
context
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Authenticated free tier user accessing free pattern
|
|
173
|
-
return {
|
|
174
|
-
allowed: true,
|
|
175
|
-
code: 'free_entitled',
|
|
176
|
-
reason: 'Free tier pattern access granted.',
|
|
177
|
-
context
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function filterAccessibleSkills(skillIds, options = {}) {
|
|
182
|
-
const allowed = [];
|
|
183
|
-
const denied = [];
|
|
184
|
-
|
|
185
|
-
for (const skillId of skillIds || []) {
|
|
186
|
-
const decision = checkSkillAccess(skillId, options);
|
|
187
|
-
if (decision.allowed) {
|
|
188
|
-
allowed.push(skillId);
|
|
189
|
-
} else {
|
|
190
|
-
denied.push({
|
|
191
|
-
skillId,
|
|
192
|
-
code: decision.code,
|
|
193
|
-
reason: decision.reason
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return { allowed, denied };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function isPremiumWorkflow(workflow) {
|
|
202
|
-
const tier = String(workflow?.tier || 'free').trim().toLowerCase();
|
|
203
|
-
return tier !== 'free';
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Lazy-load pack lifecycle to avoid circular dependency
|
|
207
|
-
let _packLifecycle = null;
|
|
208
|
-
function getPackLifecycle() {
|
|
209
|
-
if (!_packLifecycle) {
|
|
210
|
-
try {
|
|
211
|
-
_packLifecycle = require('../intelligence/orchestrator/config/pack-lifecycle');
|
|
212
|
-
} catch {
|
|
213
|
-
_packLifecycle = { isPackVisibleToUser: () => true, getPackStage: () => 'ga' };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return _packLifecycle;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function checkWorkflowAccess(workflow, options = {}) {
|
|
220
|
-
const context = resolveWorkflowAccessContext(options);
|
|
221
|
-
const policy = policies.getPolicyProfile(context.policyProfile, options);
|
|
222
|
-
|
|
223
|
-
// Check if workflow is blocked by policy
|
|
224
|
-
if (policies.isWorkflowBlocked(workflow, policy)) {
|
|
225
|
-
return {
|
|
226
|
-
allowed: false,
|
|
227
|
-
code: 'workflow_policy_blocked',
|
|
228
|
-
reason: `Workflow ${workflow?.key || workflow?.name || 'unknown'} is blocked by ${policy.id} policy profile.`,
|
|
229
|
-
context
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Check pack lifecycle visibility (for premium packs)
|
|
234
|
-
const packName = workflow?.pack;
|
|
235
|
-
if (packName) {
|
|
236
|
-
const lifecycle = getPackLifecycle();
|
|
237
|
-
const isVisible = lifecycle.isPackVisibleToUser(packName, {
|
|
238
|
-
tier: context.tier,
|
|
239
|
-
userId: options.userId,
|
|
240
|
-
deviceId: options.deviceId
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
if (!isVisible) {
|
|
244
|
-
const stage = lifecycle.getPackStage(packName);
|
|
245
|
-
return {
|
|
246
|
-
allowed: false,
|
|
247
|
-
code: 'pack_not_available',
|
|
248
|
-
reason: `Pack "${packName}" is not available in your rollout group (stage: ${stage})`,
|
|
249
|
-
context,
|
|
250
|
-
stage
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (!isPremiumWorkflow(workflow)) {
|
|
256
|
-
return {
|
|
257
|
-
allowed: true,
|
|
258
|
-
code: 'workflow_free',
|
|
259
|
-
reason: 'Workflow is available on free tier.',
|
|
260
|
-
context
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (context.mode !== SERVER_MODE) {
|
|
265
|
-
return {
|
|
266
|
-
allowed: true,
|
|
267
|
-
code: 'workflow_local_mode',
|
|
268
|
-
reason: 'Premium workflows are enabled in local mode.',
|
|
269
|
-
context
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (context.entitled || isProTier(context.tier)) {
|
|
274
|
-
return {
|
|
275
|
-
allowed: true,
|
|
276
|
-
code: 'workflow_entitled',
|
|
277
|
-
reason: 'Premium workflow access granted.',
|
|
278
|
-
context
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
allowed: false,
|
|
284
|
-
code: 'workflow_subscription_required',
|
|
285
|
-
reason: 'Premium workflows require entitlement in server mode. Set BOOTSPRING_WORKFLOWS_ENTITLED=true or use tier=pro/team/enterprise.',
|
|
286
|
-
context
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function filterAccessibleWorkflows(workflows, options = {}) {
|
|
291
|
-
const allowed = [];
|
|
292
|
-
const denied = [];
|
|
293
|
-
|
|
294
|
-
for (const workflow of workflows || []) {
|
|
295
|
-
const decision = checkWorkflowAccess(workflow, options);
|
|
296
|
-
if (decision.allowed) {
|
|
297
|
-
allowed.push(workflow);
|
|
298
|
-
} else {
|
|
299
|
-
denied.push({
|
|
300
|
-
key: workflow?.key,
|
|
301
|
-
name: workflow?.name,
|
|
302
|
-
code: decision.code,
|
|
303
|
-
reason: decision.reason
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return { allowed, denied };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
module.exports = {
|
|
312
|
-
LOCAL_MODE,
|
|
313
|
-
SERVER_MODE,
|
|
314
|
-
resolveAccessContext,
|
|
315
|
-
resolveWorkflowAccessContext,
|
|
316
|
-
isExternalSkill,
|
|
317
|
-
checkSkillAccess,
|
|
318
|
-
filterAccessibleSkills,
|
|
319
|
-
isPremiumWorkflow,
|
|
320
|
-
checkWorkflowAccess,
|
|
321
|
-
filterAccessibleWorkflows
|
|
322
|
-
};
|