@girardmedia/bootspring 2.0.22 → 2.0.23
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/bin/bootspring.js +5 -0
- package/cli/org.js +474 -0
- package/cli/preseed.js +9 -301
- package/cli/seed.js +23 -1074
- package/core/api-client.js +77 -0
- package/core/entitlements.js +36 -0
- package/core/organizations.js +223 -0
- package/core/policies.js +51 -6
- package/core/policy-matrix.js +303 -0
- package/core/project-context.js +1 -0
- package/intelligence/orchestrator/config/index.js +4 -1
- package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
- package/intelligence/orchestrator.js +17 -512
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +1 -1
package/core/api-client.js
CHANGED
|
@@ -944,6 +944,83 @@ const api = {
|
|
|
944
944
|
async getPreseedWizard(projectId) {
|
|
945
945
|
requireAuth();
|
|
946
946
|
return directRequest('GET', `/projects/${encodeURIComponent(projectId)}/preseed/wizard`);
|
|
947
|
+
},
|
|
948
|
+
|
|
949
|
+
// =====================================================
|
|
950
|
+
// Organization Methods
|
|
951
|
+
// =====================================================
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* List organizations for current user
|
|
955
|
+
* @returns {Promise<Array>} List of organizations
|
|
956
|
+
*/
|
|
957
|
+
async listOrganizations() {
|
|
958
|
+
requireAuth();
|
|
959
|
+
return request('GET', '/organizations');
|
|
960
|
+
},
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Get organization details
|
|
964
|
+
* @param {string} orgId - Organization ID
|
|
965
|
+
* @returns {Promise<object>} Organization details with members
|
|
966
|
+
*/
|
|
967
|
+
async getOrganization(orgId) {
|
|
968
|
+
requireAuth();
|
|
969
|
+
return request('GET', `/organizations/${encodeURIComponent(orgId)}`);
|
|
970
|
+
},
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Get organization policy
|
|
974
|
+
* @param {string} orgId - Organization ID
|
|
975
|
+
* @returns {Promise<object>} Organization policy settings
|
|
976
|
+
*/
|
|
977
|
+
async getOrgPolicy(orgId) {
|
|
978
|
+
requireAuth();
|
|
979
|
+
return request('GET', `/organizations/${encodeURIComponent(orgId)}/policy`);
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Update organization policy
|
|
984
|
+
* @param {string} orgId - Organization ID
|
|
985
|
+
* @param {object} policy - Policy settings to update
|
|
986
|
+
* @returns {Promise<object>} Updated policy
|
|
987
|
+
*/
|
|
988
|
+
async updateOrgPolicy(orgId, policy) {
|
|
989
|
+
requireAuth();
|
|
990
|
+
return request('PATCH', `/organizations/${encodeURIComponent(orgId)}/policy`, policy);
|
|
991
|
+
},
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* List organization members
|
|
995
|
+
* @param {string} orgId - Organization ID
|
|
996
|
+
* @returns {Promise<Array>} List of members
|
|
997
|
+
*/
|
|
998
|
+
async listOrgMembers(orgId) {
|
|
999
|
+
requireAuth();
|
|
1000
|
+
return request('GET', `/organizations/${encodeURIComponent(orgId)}/members`);
|
|
1001
|
+
},
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Get member policy overrides
|
|
1005
|
+
* @param {string} orgId - Organization ID
|
|
1006
|
+
* @param {string} userId - User ID
|
|
1007
|
+
* @returns {Promise<object>} Member policy overrides
|
|
1008
|
+
*/
|
|
1009
|
+
async getMemberPolicy(orgId, userId) {
|
|
1010
|
+
requireAuth();
|
|
1011
|
+
return request('GET', `/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}/policy`);
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Update member policy overrides
|
|
1016
|
+
* @param {string} orgId - Organization ID
|
|
1017
|
+
* @param {string} userId - User ID
|
|
1018
|
+
* @param {object} policy - Policy overrides
|
|
1019
|
+
* @returns {Promise<object>} Updated member policy
|
|
1020
|
+
*/
|
|
1021
|
+
async updateMemberPolicy(orgId, userId, policy) {
|
|
1022
|
+
requireAuth();
|
|
1023
|
+
return request('PATCH', `/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}/policy`, policy);
|
|
947
1024
|
}
|
|
948
1025
|
};
|
|
949
1026
|
|
package/core/entitlements.js
CHANGED
|
@@ -197,10 +197,24 @@ function isPremiumWorkflow(workflow) {
|
|
|
197
197
|
return tier !== 'free';
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// Lazy-load pack lifecycle to avoid circular dependency
|
|
201
|
+
let _packLifecycle = null;
|
|
202
|
+
function getPackLifecycle() {
|
|
203
|
+
if (!_packLifecycle) {
|
|
204
|
+
try {
|
|
205
|
+
_packLifecycle = require('../intelligence/orchestrator/config/pack-lifecycle');
|
|
206
|
+
} catch {
|
|
207
|
+
_packLifecycle = { isPackVisibleToUser: () => true, getPackStage: () => 'ga' };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return _packLifecycle;
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
function checkWorkflowAccess(workflow, options = {}) {
|
|
201
214
|
const context = resolveWorkflowAccessContext(options);
|
|
202
215
|
const policy = policies.getPolicyProfile(context.policyProfile, options);
|
|
203
216
|
|
|
217
|
+
// Check if workflow is blocked by policy
|
|
204
218
|
if (policies.isWorkflowBlocked(workflow, policy)) {
|
|
205
219
|
return {
|
|
206
220
|
allowed: false,
|
|
@@ -210,6 +224,28 @@ function checkWorkflowAccess(workflow, options = {}) {
|
|
|
210
224
|
};
|
|
211
225
|
}
|
|
212
226
|
|
|
227
|
+
// Check pack lifecycle visibility (for premium packs)
|
|
228
|
+
const packName = workflow?.pack;
|
|
229
|
+
if (packName) {
|
|
230
|
+
const lifecycle = getPackLifecycle();
|
|
231
|
+
const isVisible = lifecycle.isPackVisibleToUser(packName, {
|
|
232
|
+
tier: context.tier,
|
|
233
|
+
userId: options.userId,
|
|
234
|
+
deviceId: options.deviceId
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!isVisible) {
|
|
238
|
+
const stage = lifecycle.getPackStage(packName);
|
|
239
|
+
return {
|
|
240
|
+
allowed: false,
|
|
241
|
+
code: 'pack_not_available',
|
|
242
|
+
reason: `Pack "${packName}" is not available in your rollout group (stage: ${stage})`,
|
|
243
|
+
context,
|
|
244
|
+
stage
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
213
249
|
if (!isPremiumWorkflow(workflow)) {
|
|
214
250
|
return {
|
|
215
251
|
allowed: true,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organizations Module
|
|
3
|
+
* Org-level policy resolution and caching
|
|
4
|
+
* @package bootspring
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const apiClient = require('./api-client');
|
|
8
|
+
const policyMatrix = require('./policy-matrix');
|
|
9
|
+
const auth = require('./auth');
|
|
10
|
+
|
|
11
|
+
// Cache for org data (5 minute TTL)
|
|
12
|
+
const ORG_CACHE_TTL = 5 * 60 * 1000;
|
|
13
|
+
const orgCache = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Organization structure
|
|
17
|
+
* @typedef {object} Organization
|
|
18
|
+
* @property {string} id - Organization ID
|
|
19
|
+
* @property {string} name - Organization name
|
|
20
|
+
* @property {string} tier - Subscription tier (team/enterprise)
|
|
21
|
+
* @property {string} policyProfile - Default policy profile
|
|
22
|
+
* @property {object} settings - Org settings
|
|
23
|
+
* @property {OrgMember[]} members - Organization members
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Organization member structure
|
|
28
|
+
* @typedef {object} OrgMember
|
|
29
|
+
* @property {string} userId - User ID
|
|
30
|
+
* @property {string} email - User email
|
|
31
|
+
* @property {string} role - Member role (owner/admin/member/viewer)
|
|
32
|
+
* @property {object} policyOverrides - Per-member policy overrides
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get organization from cache or API
|
|
37
|
+
* @param {string} orgId - Organization ID
|
|
38
|
+
* @param {object} options - Options including apiKey
|
|
39
|
+
* @returns {Promise<Organization|null>}
|
|
40
|
+
*/
|
|
41
|
+
async function getOrganization(orgId, options = {}) {
|
|
42
|
+
if (!orgId) return null;
|
|
43
|
+
|
|
44
|
+
const cacheKey = `org:${orgId}`;
|
|
45
|
+
const cached = orgCache.get(cacheKey);
|
|
46
|
+
|
|
47
|
+
if (cached && Date.now() - cached.timestamp < ORG_CACHE_TTL) {
|
|
48
|
+
return cached.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const org = await apiClient.getOrganization(orgId, options);
|
|
53
|
+
if (org) {
|
|
54
|
+
orgCache.set(cacheKey, { data: org, timestamp: Date.now() });
|
|
55
|
+
}
|
|
56
|
+
return org;
|
|
57
|
+
} catch {
|
|
58
|
+
// Return cached data if available, even if stale
|
|
59
|
+
return cached?.data || null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get organization member
|
|
65
|
+
* @param {string} orgId - Organization ID
|
|
66
|
+
* @param {string} userId - User ID
|
|
67
|
+
* @param {object} options - Options
|
|
68
|
+
* @returns {Promise<OrgMember|null>}
|
|
69
|
+
*/
|
|
70
|
+
async function getOrgMember(orgId, userId, options = {}) {
|
|
71
|
+
const org = await getOrganization(orgId, options);
|
|
72
|
+
if (!org?.members) return null;
|
|
73
|
+
return org.members.find(m => m.userId === userId) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve organization context from credentials/env
|
|
78
|
+
* @param {object} options - Options
|
|
79
|
+
* @returns {Promise<object>} Org context
|
|
80
|
+
*/
|
|
81
|
+
async function resolveOrgContext(options = {}) {
|
|
82
|
+
// Try to get org ID from various sources
|
|
83
|
+
const orgId = options.orgId
|
|
84
|
+
|| process.env.BOOTSPRING_ORG_ID
|
|
85
|
+
|| auth.getCredentials()?.orgId;
|
|
86
|
+
|
|
87
|
+
if (!orgId) {
|
|
88
|
+
return {
|
|
89
|
+
hasOrg: false,
|
|
90
|
+
orgId: null,
|
|
91
|
+
org: null,
|
|
92
|
+
member: null,
|
|
93
|
+
policy: null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const org = await getOrganization(orgId, options);
|
|
98
|
+
if (!org) {
|
|
99
|
+
return {
|
|
100
|
+
hasOrg: false,
|
|
101
|
+
orgId,
|
|
102
|
+
org: null,
|
|
103
|
+
member: null,
|
|
104
|
+
policy: null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get current user's membership
|
|
109
|
+
const userId = options.userId || auth.getCredentials()?.userId;
|
|
110
|
+
const member = userId ? await getOrgMember(orgId, userId, options) : null;
|
|
111
|
+
|
|
112
|
+
// Build effective policy
|
|
113
|
+
const policy = policyMatrix.buildEffectivePolicy(
|
|
114
|
+
org.tier || 'team',
|
|
115
|
+
org.policyProfile || 'startup',
|
|
116
|
+
member?.policyOverrides || {}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
hasOrg: true,
|
|
121
|
+
orgId,
|
|
122
|
+
org,
|
|
123
|
+
member,
|
|
124
|
+
policy,
|
|
125
|
+
role: member?.role || 'viewer'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if user has permission in org
|
|
131
|
+
* @param {string} permission - Permission to check
|
|
132
|
+
* @param {object} orgContext - Organization context
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
function hasOrgPermission(permission, orgContext) {
|
|
136
|
+
if (!orgContext?.hasOrg) return false;
|
|
137
|
+
|
|
138
|
+
const role = orgContext.role || 'viewer';
|
|
139
|
+
const rolePerms = policyMatrix.ROLE_PERMISSIONS[role];
|
|
140
|
+
|
|
141
|
+
if (!rolePerms) return false;
|
|
142
|
+
return rolePerms[permission] === true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check policy access for a scope
|
|
147
|
+
* @param {string} scope - Scope to check (e.g., 'skills.external')
|
|
148
|
+
* @param {object} options - Options including orgId
|
|
149
|
+
* @returns {Promise<object>} Access result
|
|
150
|
+
*/
|
|
151
|
+
async function checkOrgPolicyAccess(scope, options = {}) {
|
|
152
|
+
const orgContext = await resolveOrgContext(options);
|
|
153
|
+
|
|
154
|
+
// If no org, fall back to user-level policy
|
|
155
|
+
if (!orgContext.hasOrg || !orgContext.policy) {
|
|
156
|
+
return {
|
|
157
|
+
allowed: true,
|
|
158
|
+
hasOrgPolicy: false,
|
|
159
|
+
scope
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = policyMatrix.checkPolicyAccess(scope, orgContext.policy);
|
|
164
|
+
return {
|
|
165
|
+
...result,
|
|
166
|
+
hasOrgPolicy: true,
|
|
167
|
+
orgId: orgContext.orgId,
|
|
168
|
+
tier: orgContext.policy.tier,
|
|
169
|
+
profile: orgContext.policy.profile
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get org policy summary for display
|
|
175
|
+
* @param {object} options - Options
|
|
176
|
+
* @returns {Promise<object>} Policy summary
|
|
177
|
+
*/
|
|
178
|
+
async function getOrgPolicySummary(options = {}) {
|
|
179
|
+
const orgContext = await resolveOrgContext(options);
|
|
180
|
+
|
|
181
|
+
if (!orgContext.hasOrg) {
|
|
182
|
+
return {
|
|
183
|
+
hasOrg: false,
|
|
184
|
+
message: 'No organization context'
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const policy = orgContext.policy;
|
|
189
|
+
return {
|
|
190
|
+
hasOrg: true,
|
|
191
|
+
orgId: orgContext.orgId,
|
|
192
|
+
orgName: orgContext.org?.name,
|
|
193
|
+
tier: policy.tier,
|
|
194
|
+
profile: policy.profile,
|
|
195
|
+
role: orgContext.role,
|
|
196
|
+
allowedScopes: policy.allowed.length,
|
|
197
|
+
blockedScopes: policy.blocked.length,
|
|
198
|
+
limits: policy.limits,
|
|
199
|
+
overrides: Object.keys(policy.overrides)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Clear organization cache
|
|
205
|
+
* @param {string} [orgId] - Specific org to clear, or all if not specified
|
|
206
|
+
*/
|
|
207
|
+
function clearOrgCache(orgId) {
|
|
208
|
+
if (orgId) {
|
|
209
|
+
orgCache.delete(`org:${orgId}`);
|
|
210
|
+
} else {
|
|
211
|
+
orgCache.clear();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
getOrganization,
|
|
217
|
+
getOrgMember,
|
|
218
|
+
resolveOrgContext,
|
|
219
|
+
hasOrgPermission,
|
|
220
|
+
checkOrgPolicyAccess,
|
|
221
|
+
getOrgPolicySummary,
|
|
222
|
+
clearOrgCache
|
|
223
|
+
};
|
package/core/policies.js
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bootspring policy profiles
|
|
3
|
-
* Team-level controls for capability gating.
|
|
3
|
+
* Team-level and org-level controls for capability gating.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const policyMatrix = require('./policy-matrix');
|
|
7
|
+
|
|
6
8
|
const DEFAULT_POLICY_PROFILE = 'startup';
|
|
7
9
|
|
|
8
10
|
const POLICY_PROFILES = {
|
|
9
11
|
startup: {
|
|
10
12
|
id: 'startup',
|
|
11
13
|
name: 'Startup',
|
|
14
|
+
description: 'Permissive policy for fast iteration',
|
|
12
15
|
allowExternalSkills: true,
|
|
13
|
-
blockedWorkflows: []
|
|
16
|
+
blockedWorkflows: [],
|
|
17
|
+
tier: 'any'
|
|
14
18
|
},
|
|
15
19
|
regulated: {
|
|
16
20
|
id: 'regulated',
|
|
17
21
|
name: 'Regulated',
|
|
22
|
+
description: 'Compliance-focused with external skill restrictions',
|
|
18
23
|
allowExternalSkills: false,
|
|
19
|
-
blockedWorkflows: ['growth-pack']
|
|
24
|
+
blockedWorkflows: ['growth-pack'],
|
|
25
|
+
tier: 'team'
|
|
20
26
|
},
|
|
21
27
|
enterprise: {
|
|
22
28
|
id: 'enterprise',
|
|
23
29
|
name: 'Enterprise',
|
|
30
|
+
description: 'Full control with SSO and audit requirements',
|
|
24
31
|
allowExternalSkills: true,
|
|
25
|
-
blockedWorkflows: []
|
|
32
|
+
blockedWorkflows: [],
|
|
33
|
+
tier: 'enterprise'
|
|
26
34
|
}
|
|
27
35
|
};
|
|
28
36
|
|
|
@@ -54,15 +62,52 @@ function getPolicyProfile(profile, options = {}) {
|
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
function isWorkflowBlocked(workflow, profile) {
|
|
57
|
-
const workflowKey = String(workflow?.key || '').trim();
|
|
65
|
+
const workflowKey = String(workflow?.key || workflow || '').trim();
|
|
58
66
|
if (!workflowKey) return false;
|
|
59
67
|
return (profile.blockedWorkflows || []).includes(workflowKey);
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Check if a scope is blocked by policy
|
|
72
|
+
* @param {string} scope - Scope to check (e.g., 'skills.external')
|
|
73
|
+
* @param {string} tier - Current tier
|
|
74
|
+
* @param {string} profile - Policy profile
|
|
75
|
+
* @param {object} memberOverrides - Per-member overrides
|
|
76
|
+
* @returns {object} Access result
|
|
77
|
+
*/
|
|
78
|
+
function checkScopeAccess(scope, tier = 'free', profile = 'startup', memberOverrides = {}) {
|
|
79
|
+
const effectivePolicy = policyMatrix.buildEffectivePolicy(tier, profile, memberOverrides);
|
|
80
|
+
return policyMatrix.checkPolicyAccess(scope, effectivePolicy);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all available policy profiles
|
|
85
|
+
* @returns {object[]} List of profiles
|
|
86
|
+
*/
|
|
87
|
+
function listPolicyProfiles() {
|
|
88
|
+
return Object.values(POLICY_PROFILES).map(p => ({
|
|
89
|
+
id: p.id,
|
|
90
|
+
name: p.name,
|
|
91
|
+
description: p.description,
|
|
92
|
+
tier: p.tier
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get policy scopes
|
|
98
|
+
* @returns {object} Available scopes
|
|
99
|
+
*/
|
|
100
|
+
function getPolicyScopes() {
|
|
101
|
+
return policyMatrix.POLICY_SCOPES;
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
module.exports = {
|
|
63
105
|
DEFAULT_POLICY_PROFILE,
|
|
64
106
|
POLICY_PROFILES,
|
|
65
107
|
resolvePolicyProfile,
|
|
66
108
|
getPolicyProfile,
|
|
67
|
-
isWorkflowBlocked
|
|
109
|
+
isWorkflowBlocked,
|
|
110
|
+
checkScopeAccess,
|
|
111
|
+
listPolicyProfiles,
|
|
112
|
+
getPolicyScopes
|
|
68
113
|
};
|