Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ac17b0b
Move round feature enablement
hardillb Aug 18, 2026
a768c89
Add new types file
hardillb Aug 11, 2026
1fe1942
remove duplicate sso enablement
hardillb Aug 11, 2026
3f98c99
Second attempt at types
hardillb Aug 11, 2026
ce97340
fix lint
hardillb Aug 11, 2026
2ffa20c
more lint
hardillb Aug 11, 2026
f402883
Add new license levels
hardillb Aug 12, 2026
0767a18
Move round feature enablement
hardillb Aug 10, 2026
e9e6f8d
remove duplicate sso enablement
hardillb Aug 11, 2026
e3ca44a
fix lint
hardillb Aug 11, 2026
8f141c3
Rework
hardillb Aug 20, 2026
2fbc792
Fix lint
hardillb Aug 20, 2026
278ecc3
generate types
hardillb Aug 20, 2026
896b5d9
Move BOM to enterpise and hub only
hardillb Aug 24, 2026
4ea3fcb
Fix tests
hardillb Aug 24, 2026
f800c6f
Fix missing tests license check
hardillb Aug 24, 2026
6484bea
Add the new types
hardillb Aug 24, 2026
282759d
Move customhostnames to hub/enterprise only
hardillb Aug 25, 2026
eea1cdf
remvoe teambroker from hub
hardillb Aug 25, 2026
28427ad
Add license tests
hardillb Aug 25, 2026
892b69e
remove only
hardillb Aug 25, 2026
78d997e
Last lint on the tests
hardillb Aug 25, 2026
77d216b
Split hosted/remote questions and apply defaults based on tier
knolleary Aug 26, 2026
e0b37ef
Update forge/ee/lib/index.js
hardillb Aug 26, 2026
935d3ce
Merge branch 'main' into new-license-tiers
knolleary Aug 26, 2026
266e8a4
Move overage allowance checking to license handling code
knolleary Aug 26, 2026
2696dbb
Fix up version checks when enabling feature sets
knolleary Aug 26, 2026
cf59cd6
Add remoteInstances feature flag through UX
knolleary Aug 26, 2026
853e127
Merge branch 'main' into new-license-tiers
hardillb Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions forge/db/models/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,15 @@ module.exports = {
hooks: function (M, app) {
return {
beforeCreate: async (device, options) => {
// if the product is licensed, we permit overage
const isLicensed = app.license.active()
if (isLicensed !== true) {
const { devices } = await app.license.usage('devices')
if (devices.count >= devices.limit) {
if (app.license.active() && app.license.status().expired) {
throw new Error('license expired')
}
const { devices } = await app.license.usage('devices')
if (devices.count >= devices.limit) {
// Potential overage - check if overage is permitted by the license
if (!app.license.allowOverage('devices')) {
throw new Error('license limit reached')
}
} else {
if (app.license.status().expired) {
throw new Error('license expired')
}
}
},
afterCreate: async (device, options) => {
Expand Down
12 changes: 7 additions & 5 deletions forge/db/models/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,13 @@ module.exports = {
hooks: function (M, app) {
return {
beforeCreate: async (project, opts) => {
// if the product is licensed, we permit overage
const isLicensed = app.license.active()
if (isLicensed !== true) {
const { instances } = await app.license.usage('instances')
if (instances.count >= instances.limit) {
if (app.license.active() && app.license.status().expired) {
throw new Error('license expired')
}
const { instances } = await app.license.usage('instances')
if (instances.count >= instances.limit) {
// Potential overage - check if overage is permitted by the license
if (!app.license.allowOverage('instances')) {
throw new Error('license limit reached')
}
}
Expand Down
150 changes: 94 additions & 56 deletions forge/ee/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,110 @@
const fp = require('fastify-plugin')

module.exports = fp(async function (app, opts) {
if (app.config.billing) {
app.decorate('billing', await require('./billing').init(app))
// common features across all higher tier (enterpise, hub, edge, fleet)
async function commonFeatures (app, opts) {
app.decorate('sso', await require('./sso').init(app))
// Set the MFA Feature Flag
app.config.features.register('mfa', true, true)
// Set the Project History timeline Feature Flag
app.config.features.register('projectHistory', true, true)
if (app.config.npmRegistry?.enabled) {
// Set npm Feature Flag
app.config.features.register('npm', true, true)
}
require('./projectComms').init(app)
require('./deviceEditor').init(app)
require('./alerts').init(app)
if (app.config.tables?.enabled) {
app.decorate('tables', await require('./tables').init(app))
}
app.config.features.register('certifiedNodes', true, true)
app.config.features.register('ffNodes', true, true)
// Application level RBAC
app.config.features.register('rbacApplication', true, true)
require('./autoUpdateStacks').init(app)

if (app.license.get('tier') === 'enterprise') {
require('./ha').init(app)
require('./protectedInstance').init(app)
require('./customHostnames').init(app)
app.decorate('sso', await require('./sso').init(app))
await require('./teamBroker').init(app)
app.decorate('gitops', await require('./gitops').init(app))
// Set the MFA Feature Flag
app.config.features.register('mfa', true, true)
// Set the Device Groups Feature Flag
app.config.features.register('deviceGroups', true, true)
// Set the Project History timeline Feature Flag
app.config.features.register('projectHistory', true, true)
// Set the Bill of Materials Feature Flag
app.config.features.register('bom', true, true)
if (app.config.npmRegistry?.enabled) {
// Set npm Feature Flag
app.config.features.register('npm', true, true)
}
if (app.config.tables?.enabled) {
app.decorate('tables', await require('./tables').init(app))
}
app.config.features.register('certifiedNodes', true, true)
app.config.features.register('ffNodes', true, true)
app.config.features.register('rbacApplication', true, true)
require('./autoUpdateStacks').init(app)
// Expert
await app.register(require('./expert'))

// Expert
await app.register(require('./expert'))
// Set the AI Features Flag (global gate for all AI features)
const isAiEnabled = !!(app.config?.ai?.enabled ?? true)
app.config.features.register('ai', isAiEnabled, true)

// Set the AI Features Flag (global gate for all AI features)
const isAiEnabled = !!(app.config?.ai?.enabled ?? true)
app.config.features.register('ai', isAiEnabled, true)
// Set the Generate Snapshot Description Feature Flag
const isAssistantConfigured = isAiEnabled && app.config.assistant?.enabled === true && !!app.config.assistant?.service?.url
app.config.features.register('generatedSnapshotDescription', isAssistantConfigured, true)

// Set the Generate Snapshot Description Feature Flag
const isAssistantConfigured = isAiEnabled && app.config.assistant?.enabled === true && !!app.config.assistant?.service?.url
app.config.features.register('generatedSnapshotDescription', isAssistantConfigured, true)
// Set the assistant inline completions Feature Flag
app.config.features.register('assistantInlineCompletions', isAssistantConfigured, true)

// Set the assistant inline completions Feature Flag
app.config.features.register('assistantInlineCompletions', isAssistantConfigured, true)
// Set the expert platform automation Feature Flag (MCP platform tools server)
app.config.features.register('expertPlatformAutomation', isAiEnabled && (app.config?.expert?.enabled ?? false), true)

// Set the expert platform automation Feature Flag (MCP platform tools server)
app.config.features.register('expertPlatformAutomation', isAiEnabled && (app.config?.expert?.enabled ?? false), true)
// Set the expert assistant Feature Flag
app.config.features.register('expertAssistant', isAiEnabled && (app.config?.expert?.enabled ?? false), true)

// Set the expert assistant Feature Flag
app.config.features.register('expertAssistant', isAiEnabled && (app.config?.expert?.enabled ?? false), true)
// Set the Expert Insights flag
const isInsightsEnabled = isAiEnabled &&
!!app.config?.expert?.enabled &&
(Object.prototype.hasOwnProperty.call(app.config?.expert ?? {}, 'insights') ? !!app.config?.expert?.insights?.enabled : true)

// Set the Expert Insights flag
const isInsightsEnabled = isAiEnabled &&
!!app.config?.expert?.enabled &&
(Object.prototype.hasOwnProperty.call(app.config?.expert ?? {}, 'insights') ? !!app.config?.expert?.insights?.enabled : true)
app.config.features.register('expertInsights', isInsightsEnabled ?? false, true)

app.config.features.register('expertInsights', isInsightsEnabled ?? false, true)
// Set the MCP third-party agent access flag. Defaults to true when AI
// and expert are both enabled, so it activates automatically unless
// explicitly disabled in the config.
const isMcpThirdPartyEnabled = isAiEnabled && (app.config?.expert?.enabled ?? false)
app.config.features.register('mcpThirdParty', isMcpThirdPartyEnabled, true)
}

// Set the MCP third-party agent access flag. Defaults to true when AI
// and expert are both enabled, so it activates automatically unless
// explicitly disabled in the config.
const isMcpThirdPartyEnabled = isAiEnabled && (app.config?.expert?.enabled ?? false)
app.config.features.register('mcpThirdParty', isMcpThirdPartyEnabled, true)
module.exports = fp(async function (app, opts) {
if (app.config.billing) {
app.decorate('billing', await require('./billing').init(app))
}
// common features for all license levels (inc Pro)
require('./projectComms').init(app)
require('./deviceEditor').init(app)
require('./alerts').init(app)
if (app.license.get('tier') && (app.license.get('version') === '' || app.license.get('version') === '2024-03-04')) {
app.config.features.register('remoteInstances', true, true)
if (app.license.get('tier') === 'enterprise') {
await commonFeatures(app, opts)
// HA
require('./ha').init(app)
// Protected Instances
require('./protectedInstance').init(app)
// Git Opps
app.decorate('gitops', await require('./gitops').init(app))
require('./customHostnames').init(app)
await require('./teamBroker').init(app)

// Set the Device Groups Feature Flag
app.config.features.register('deviceGroups', true, true)
// Set the Bill of Materials Feature Flag
app.config.features.register('bom', true, true)
} else {
// old "Pro" license
}
} else if (app.license.get('tiers') && app.license.get('version') === '2026-08-20') {
const tiers = app.license.get('tiers')
await commonFeatures(app, opts)

if (tiers.includes('hub')) {
// `hub` does not include remote instances so we disable the feature flag for it
// - it will get re-enabled below if the license also includes `edge` or `fleet`
app.config.features.register('remoteInstances', false, true)
// HA
require('./ha').init(app)
// Protected Instances
require('./protectedInstance').init(app)
// Git Opps
app.decorate('gitops', await require('./gitops').init(app))
// Set the Bill of Materials Feature Flag
app.config.features.register('bom', true, true)
}
if (tiers.includes('edge') || tiers.includes('fleet')) {
app.config.features.register('remoteInstances', true, true)
// Set the Device Groups Feature Flag
app.config.features.register('deviceGroups', true, true)
await require('./teamBroker').init(app)
}
}

// Set the Team Library Feature Flag
Expand Down
82 changes: 58 additions & 24 deletions forge/ee/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,79 @@
* @namespace api
* @memberof forge.ee
*/

// common features across all higher tier (enterpise, hub, edge, fleet)
async function commonFeatures (app) {
await app.register(require('./expert'), { prefix: '/api/v1/expert', logLevel: app.config.logging.http })
await app.register(require('./mcp'), { logLevel: app.config.logging.http })
if (app.config.npmRegistry?.enabled) {
await app.register(require('./catalogues'), { prefix: '/api/v1/teams/:teamId', logLevel: app.config.logging.http })
}
await app.register(require('./mfa'), { prefix: '/api/v1', logLevel: app.config.logging.http })
if (app.config.tables?.enabled) {
await app.register(require('./tables'), { prefix: '/api/v1/teams/:teamId/databases', logLevel: app.config.logging.http })
}
await app.register(require('./resource'), { prefix: '/api/v1/projects/:instanceId/resources', logLevel: app.config.logging.http })
await app.register(require('./autoUpdateStacks'), { prefix: '/api/v1/projects/:projectId/autoUpdateStack', logLevel: app.config.logging.http })
await app.register(require('./httpTokens'), { prefix: '/api/v1', logLevel: app.config.logging.http })
await app.register(require('./staticAssets'), { prefix: '/api/v1/projects/:projectId/files', logLevel: app.config.logging.http })
await app.register(require('./projectHistory'), { prefix: '/api/v1/projects/:instanceId/history', logLevel: app.config.logging.http })
await app.register(require('./deviceHistory'), { prefix: '/api/v1/devices/:deviceId/history', logLevel: app.config.logging.http })
}

module.exports = async function (app) {
app.addHook('preHandler', app.verifySession)
if (app.config.billing) {
await app.register(require('./billing'), { prefix: '/ee/billing', logLevel: app.config.logging.http })
}
// Features provied to any license level (inc pro)
await app.register(require('./sharedLibrary'), { logLevel: app.config.logging.http })
await app.register(require('./pipeline'), { prefix: '/api/v1', logLevel: app.config.logging.http })
await app.register(require('./pipeline/teamPipelines.js'), { prefix: '/api/v1/teams/:teamId/pipelines', logLevel: app.config.logging.http })
await app.register(require('./deviceEditor'), { prefix: '/api/v1/devices/:deviceId/editor', logLevel: app.config.logging.http })
await app.register(require('./bom/application.js'), { prefix: '/api/v1/applications', logLevel: app.config.logging.http })
await app.register(require('./bom/team.js'), { prefix: '/api/v1/teams', logLevel: app.config.logging.http })

await app.register(require('./flowBlueprints'), { prefix: '/api/v1/flow-blueprints', logLevel: app.config.logging.http })

if (app.license.get('tier') === 'enterprise') {
await app.register(require('./applicationDeviceGroups'), { prefix: '/api/v1/applications/:applicationId/device-groups', logLevel: app.config.logging.http })
await app.register(require('./teamDeviceGroups'), { prefix: '/api/v1/teams/:teamId/device-groups', logLevel: app.config.logging.http })
await app.register(require('./ha'), { prefix: '/api/v1/projects/:projectId/ha', logLevel: app.config.logging.http })
await app.register(require('./protectedInstance'), { prefix: '/api/v1/projects/:projectId/protectInstance', logLevel: app.config.logging.http })
await app.register(require('./mfa'), { prefix: '/api/v1', logLevel: app.config.logging.http })
await app.register(require('./httpTokens'), { prefix: '/api/v1', logLevel: app.config.logging.http })
await app.register(require('./customHostnames'), { prefix: '/api/v1/projects/:projectId/customHostname', logLevel: app.config.logging.http })
await app.register(require('./staticAssets'), { prefix: '/api/v1/projects/:projectId/files', logLevel: app.config.logging.http })
await app.register(require('./projectHistory'), { prefix: '/api/v1/projects/:instanceId/history', logLevel: app.config.logging.http })
await app.register(require('./deviceHistory'), { prefix: '/api/v1/devices/:deviceId/history', logLevel: app.config.logging.http })
await app.register(require('./teamBroker'), { prefix: '/api/v1/teams/:teamId/broker', logLevel: app.config.logging.http })
await app.register(require('./teamBroker/3rdPartyBroker'), { prefix: '/api/v1/teams/:teamId/brokers', logLevel: app.config.logging.http })
if (app.config.npmRegistry?.enabled) {
await app.register(require('./catalogues'), { prefix: '/api/v1/teams/:teamId', logLevel: app.config.logging.http })
let enableSSO = false
if (app.license.get('tier') && (app.license.get('version') === '' || app.license.get('version') === '2024-03-04')) {
if (app.license.get('tier') === 'enterprise') {
await commonFeatures(app)
await app.register(require('./teamBroker'), { prefix: '/api/v1/teams/:teamId/broker', logLevel: app.config.logging.http })
await app.register(require('./teamBroker/3rdPartyBroker'), { prefix: '/api/v1/teams/:teamId/brokers', logLevel: app.config.logging.http })
await app.register(require('./applicationDeviceGroups'), { prefix: '/api/v1/applications/:applicationId/device-groups', logLevel: app.config.logging.http })
await app.register(require('./teamDeviceGroups'), { prefix: '/api/v1/teams/:teamId/device-groups', logLevel: app.config.logging.http })
await app.register(require('./ha'), { prefix: '/api/v1/projects/:projectId/ha', logLevel: app.config.logging.http })
await app.register(require('./protectedInstance'), { prefix: '/api/v1/projects/:projectId/protectInstance', logLevel: app.config.logging.http })
await app.register(require('./gitops'), { prefix: '/api/v1/teams/:teamId/git', logLevel: app.config.logging.http })
await app.register(require('./bom/application.js'), { prefix: '/api/v1/applications', logLevel: app.config.logging.http })
await app.register(require('./bom/team.js'), { prefix: '/api/v1/teams', logLevel: app.config.logging.http })
await app.register(require('./customHostnames'), { prefix: '/api/v1/projects/:projectId/customHostname', logLevel: app.config.logging.http })
enableSSO = true
} else {
// old Pro license
}
} else if (app.license.get('tiers') && app.license.get('version') === '2026-08-20') {
const tiers = app.license.get('tiers')
await commonFeatures(app)
enableSSO = true

if (tiers.includes('hub')) {
await app.register(require('./gitops'), { prefix: '/api/v1/teams/:teamId/git', logLevel: app.config.logging.http })
await app.register(require('./ha'), { prefix: '/api/v1/projects/:projectId/ha', logLevel: app.config.logging.http })
await app.register(require('./protectedInstance'), { prefix: '/api/v1/projects/:projectId/protectInstance', logLevel: app.config.logging.http })
await app.register(require('./bom/application.js'), { prefix: '/api/v1/applications', logLevel: app.config.logging.http })
await app.register(require('./bom/team.js'), { prefix: '/api/v1/teams', logLevel: app.config.logging.http })
await app.register(require('./customHostnames'), { prefix: '/api/v1/projects/:projectId/customHostname', logLevel: app.config.logging.http })
}
await app.register(require('./gitops'), { prefix: '/api/v1/teams/:teamId/git', logLevel: app.config.logging.http })
await app.register(require('./resource'), { prefix: '/api/v1/projects/:instanceId/resources', logLevel: app.config.logging.http })
if (app.config.tables?.enabled) {
await app.register(require('./tables'), { prefix: '/api/v1/teams/:teamId/databases', logLevel: app.config.logging.http })
if (tiers.includes('edge') || tiers.includes('fleet')) {
await app.register(require('./teamBroker'), { prefix: '/api/v1/teams/:teamId/broker', logLevel: app.config.logging.http })
await app.register(require('./teamBroker/3rdPartyBroker'), { prefix: '/api/v1/teams/:teamId/brokers', logLevel: app.config.logging.http })
await app.register(require('./applicationDeviceGroups'), { prefix: '/api/v1/applications/:applicationId/device-groups', logLevel: app.config.logging.http })
await app.register(require('./teamDeviceGroups'), { prefix: '/api/v1/teams/:teamId/device-groups', logLevel: app.config.logging.http })
}
await app.register(require('./mcp'), { logLevel: app.config.logging.http })
await app.register(require('./autoUpdateStacks'), { prefix: '/api/v1/projects/:projectId/autoUpdateStack', logLevel: app.config.logging.http })
await app.register(require('./expert'), { prefix: '/api/v1/expert', logLevel: app.config.logging.http })
}

if (enableSSO) {
// Important: keep SSO last to avoid its error handling polluting other routes.
await app.register(require('./sso'), { logLevel: app.config.logging.http })
}
Expand Down
Loading
Loading