@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.
@@ -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
 
@@ -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
  };