@forge/feature-flags-node 1.0.0-next.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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @forge/feature-flags-node
2
+
3
+ ## 1.0.0-next.0
4
+
5
+ ### Major Changes
6
+
7
+ - ebad936: Initial release of @forge/feature-flags-node
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [ebad936]
12
+ - @forge/api@6.0.2-next.3
package/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2025 Atlassian
2
+ Permission is hereby granted to use this software in accordance with the terms
3
+ and conditions outlined in the Atlassian Developer Terms, which can be found
4
+ at the following URL:
5
+ https://developer.atlassian.com/platform/marketplace/atlassian-developer-terms/
6
+ By using this software, you agree to comply with these terms and conditions.
7
+ If you do not agree with these terms, you are not permitted to use this software.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # @forge/feature-flags-node
2
+
3
+ A feature flag SDK for Atlassian Forge apps running on Forge Node Runtime. This package provides a simple interface for evaluating feature flags.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ yarn add @forge/feature-flags-node
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage
14
+
15
+ ```typescript
16
+ import { getAppContext } from "@forge/api";
17
+ import { ForgeFeatureFlags } from "@forge/feature-flags-node";
18
+
19
+ export const handler = async (payload, context) => {
20
+ const { environmentType } = getAppContext();
21
+
22
+ // Initialize the feature flags service
23
+ const featureFlags = new ForgeFeatureFlags();
24
+ await featureFlags.initialize({
25
+ environment: environmentType?.toLowerCase() || "development" // Optional, defaults to 'development'
26
+ });
27
+
28
+ // Define a user
29
+ const user = {
30
+ userID: context?.principal?.accountId,
31
+ custom: {
32
+ license: context?.license?.isActive
33
+ }
34
+ };
35
+
36
+ // Check a feature flag (synchronous after initialization)
37
+ const isEnabled = featureFlags.checkFlag(user, "new-feature");
38
+
39
+ // Get multiple flags at once (synchronous)
40
+ const flags = featureFlags.getFeatureFlags(user, ["feature-a", "feature-b"]);
41
+
42
+ // Shutdown when done
43
+ await featureFlags.shutdown();
44
+ }
45
+
46
+ ```
47
+
48
+ ## ⚠️ Important: Initialization Best Practices
49
+
50
+ **ALWAYS initialize the ForgeFeatureFlags SDK inside your handler function, NEVER initialize globally as it will use the stale feature flags**
51
+
52
+ ### ✅ Correct Pattern - Initialize Inside Handler
53
+
54
+ ```typescript
55
+ import { getAppContext } from "@forge/api";
56
+ import { ForgeFeatureFlags } from "@forge/feature-flags-node";
57
+
58
+ export const handler = async (payload, context) => {
59
+ // ✅ Initialize inside the handler function
60
+ const featureFlags = new ForgeFeatureFlags();
61
+ await featureFlags.initialize({
62
+ environment: getAppContext().environmentType?.toLowerCase() || "development"
63
+ });
64
+
65
+ // Use feature flags...
66
+ const isEnabled = featureFlags.checkFlag(user, "new-feature");
67
+
68
+ // Shutdown when done
69
+ await featureFlags.shutdown();
70
+ }
71
+ ```
72
+
73
+ ### ❌ Incorrect Pattern - Global Initialization
74
+
75
+ ```typescript
76
+ // ❌ NEVER do this - Global initialization
77
+ const featureFlags = new ForgeFeatureFlags();
78
+ await featureFlags.initialize(); // This will cause problems!
79
+
80
+ export const handler = async (payload, context) => {
81
+ // This will fail if token expires or network issues occur
82
+ const isEnabled = featureFlags.checkFlag(user, "new-feature");
83
+ }
84
+ ```
85
+
86
+ ## Polling Behavior
87
+
88
+ The package automatically polls for feature flag updates every 30 seconds:
89
+
90
+ 1. **Cache Updates**: New data is cached for fallback purposes
91
+ 2. **Error Resilience**: If a fetch fails, the package falls back to cached data
92
+ 3. **Immediate Updates**: Feature flag changes are reflected within 30 seconds
93
+
94
+ ```typescript
95
+ // Polling happens automatically in the background
96
+ const featureFlags = new ForgeFeatureFlags();
97
+ await featureFlags.initialize();
98
+
99
+ // Feature flags are automatically updated every 30 seconds
100
+ // No manual intervention required
101
+ ```
102
+
103
+ ## API Reference
104
+
105
+ ### `ForgeFeatureFlags`
106
+
107
+ #### `constructor()`
108
+ Creates a new instance of the ForgeFeatureFlags class.
109
+
110
+ #### `initialize(config?: ForgeFeatureFlagConfig): Promise<void>`
111
+ Initializes the feature flags service with the provided configuration.
112
+
113
+ ```typescript
114
+ interface ForgeFeatureFlagConfig {
115
+ environment?: 'development' | 'staging' | 'production';
116
+ }
117
+ ```
118
+
119
+ #### `checkFlag(user: ForgeUser, flagName: string): boolean`
120
+ Checks if a feature flag is enabled for the given user. **Synchronous** after initialization.
121
+
122
+ #### `getFeatureFlags(user: ForgeUser, flagNames: string[]): Record<string, boolean>`
123
+ Gets multiple feature flags at once. **Synchronous** after initialization.
124
+
125
+ #### `shutdown(): Promise<void>`
126
+ Shuts down the feature flags service and cleans up resources.
127
+
128
+ #### `isInitialized(): boolean`
129
+ Checks if the service is initialized.
130
+
131
+ ### User Interface
132
+
133
+ ```typescript
134
+ interface ForgeUser {
135
+ userID: string;
136
+ custom?: Record<string, string | number | boolean>;
137
+ }
138
+ ```
139
+
140
+ ## Configuration Options
141
+
142
+ | Option | Type | Default | Description |
143
+ |--------|------|---------|-------------|
144
+ | `environment` | `'development' \| 'staging' \| 'production'` | `'development'` | Environment tier for feature flag evaluation |
145
+
@@ -0,0 +1,15 @@
1
+ import { IDataAdapter, AdapterResponse } from 'statsig-node';
2
+ export declare class ForgeDataAdapter implements IDataAdapter {
3
+ private readonly pollingIntervalMs;
4
+ private readonly pollingEnabled;
5
+ private readonly cache;
6
+ constructor(pollingIntervalMs?: number, pollingEnabled?: boolean);
7
+ get(key: string): Promise<AdapterResponse>;
8
+ private fetchFromAtlassianServers;
9
+ set(key: string, value: string): Promise<void>;
10
+ initialize(): Promise<void>;
11
+ shutdown(): Promise<void>;
12
+ supportsPollingUpdatesFor(key: string): boolean;
13
+ getPollingIntervalMs(): number;
14
+ }
15
+ //# sourceMappingURL=data-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-adapter.d.ts","sourceRoot":"","sources":["../src/data-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAK7D,qBAAa,gBAAiB,YAAW,YAAY;IAIjD,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc;IAJjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;gBAGrC,iBAAiB,GAAE,MAAc,EACjC,cAAc,GAAE,OAAc;IAM3C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;YA+ClC,yBAAyB;IAiCjC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU9C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ/B,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IA8B/C,oBAAoB,IAAI,MAAM;CAG/B"}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ForgeDataAdapter = void 0;
4
+ const api_1 = require("@forge/api");
5
+ class ForgeDataAdapter {
6
+ pollingIntervalMs;
7
+ pollingEnabled;
8
+ cache = new Map();
9
+ constructor(pollingIntervalMs = 30000, pollingEnabled = true) {
10
+ this.pollingIntervalMs = pollingIntervalMs;
11
+ this.pollingEnabled = pollingEnabled;
12
+ }
13
+ async get(key) {
14
+ try {
15
+ const data = await this.fetchFromAtlassianServers();
16
+ if (data) {
17
+ const serializedData = JSON.stringify(data);
18
+ this.cache.set(key, serializedData);
19
+ return {
20
+ result: serializedData,
21
+ time: Date.now()
22
+ };
23
+ }
24
+ const cachedValue = this.cache.get(key);
25
+ if (cachedValue) {
26
+ return {
27
+ result: cachedValue,
28
+ time: Date.now()
29
+ };
30
+ }
31
+ return {
32
+ result: undefined,
33
+ time: Date.now()
34
+ };
35
+ }
36
+ catch {
37
+ const cachedValue = this.cache.get(key);
38
+ if (cachedValue) {
39
+ return {
40
+ result: cachedValue,
41
+ time: Date.now()
42
+ };
43
+ }
44
+ return {
45
+ result: undefined,
46
+ time: Date.now()
47
+ };
48
+ }
49
+ }
50
+ async fetchFromAtlassianServers() {
51
+ const runtime = (0, api_1.__getRuntime)();
52
+ if (runtime.proxy?.tokenExpiry && runtime.proxy.tokenExpiry * 1000 < Date.now()) {
53
+ return null;
54
+ }
55
+ const remainingTime = runtime.lambdaContext?.getRemainingTimeInMillis?.();
56
+ if (remainingTime !== undefined && remainingTime < 5000) {
57
+ return null;
58
+ }
59
+ const response = await (0, api_1.__fetchProduct)({ provider: 'app', remote: 'feature-flags', type: 'feature-flags' })('/', {
60
+ method: 'GET',
61
+ redirect: 'follow',
62
+ headers: {
63
+ 'Content-Type': 'application/json'
64
+ }
65
+ });
66
+ if (!response.ok) {
67
+ throw new Error(`HTTP error! status: ${response.status}`);
68
+ }
69
+ return response.json();
70
+ }
71
+ async set(key, value) {
72
+ try {
73
+ this.cache.set(key, value);
74
+ }
75
+ catch { }
76
+ }
77
+ async initialize() { }
78
+ async shutdown() {
79
+ this.cache.clear();
80
+ }
81
+ supportsPollingUpdatesFor(key) {
82
+ if (!this.pollingEnabled) {
83
+ return false;
84
+ }
85
+ try {
86
+ const runtime = (0, api_1.__getRuntime)();
87
+ if (runtime.proxy?.tokenExpiry && runtime.proxy.tokenExpiry * 1000 < Date.now()) {
88
+ return false;
89
+ }
90
+ const remainingTime = runtime.lambdaContext?.getRemainingTimeInMillis?.();
91
+ if (remainingTime !== undefined && remainingTime < 10000) {
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ getPollingIntervalMs() {
101
+ return this.pollingIntervalMs;
102
+ }
103
+ }
104
+ exports.ForgeDataAdapter = ForgeDataAdapter;
package/out/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface ForgeFeatureFlagConfig {
2
+ environment?: 'development' | 'staging' | 'production';
3
+ }
4
+ export interface ForgeUser {
5
+ userID: string;
6
+ custom?: Record<string, string | number | boolean>;
7
+ }
8
+ export declare class ForgeFeatureFlags {
9
+ private initialized;
10
+ private dataAdapter;
11
+ initialize(config?: ForgeFeatureFlagConfig): Promise<void>;
12
+ checkFlag(user: ForgeUser, flagName: string): boolean;
13
+ getFeatureFlags(user: ForgeUser, flagNames: string[]): Record<string, boolean>;
14
+ shutdown(): Promise<void>;
15
+ isInitialized(): boolean;
16
+ private ensureInitialized;
17
+ private convertUser;
18
+ }
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,YAAY,CAAC;CACxD;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CACpD;AAKD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAiC;IAKvC,UAAU,CAAC,MAAM,GAAE,sBAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BpE,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQrD,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAcxE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAe/B,aAAa,IAAI,OAAO;IAI/B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,WAAW;CAMpB"}
package/out/index.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ForgeFeatureFlags = void 0;
4
+ const statsig_node_1 = require("statsig-node");
5
+ const data_adapter_1 = require("./data-adapter");
6
+ class ForgeFeatureFlags {
7
+ initialized = false;
8
+ dataAdapter = null;
9
+ async initialize(config = {}) {
10
+ if (this.initialized) {
11
+ return;
12
+ }
13
+ try {
14
+ statsig_node_1.Statsig.shutdown();
15
+ }
16
+ catch { }
17
+ this.dataAdapter = new data_adapter_1.ForgeDataAdapter(30000, true);
18
+ const statsigOptions = {
19
+ environment: { tier: config.environment || 'development' },
20
+ disableRulesetsSync: false,
21
+ disableIdListsSync: true,
22
+ initTimeoutMs: 3000,
23
+ localMode: true,
24
+ dataAdapter: this.dataAdapter
25
+ };
26
+ await statsig_node_1.Statsig.initialize('forge-internal-key', statsigOptions);
27
+ this.initialized = true;
28
+ }
29
+ checkFlag(user, flagName) {
30
+ this.ensureInitialized();
31
+ return statsig_node_1.Statsig.checkGate(this.convertUser(user), flagName);
32
+ }
33
+ getFeatureFlags(user, flagNames) {
34
+ this.ensureInitialized();
35
+ const results = {};
36
+ for (const flagName of flagNames) {
37
+ results[flagName] = statsig_node_1.Statsig.checkGate(this.convertUser(user), flagName);
38
+ }
39
+ return results;
40
+ }
41
+ async shutdown() {
42
+ if (!this.initialized) {
43
+ return;
44
+ }
45
+ statsig_node_1.Statsig.shutdown();
46
+ if (this.dataAdapter) {
47
+ await this.dataAdapter.shutdown();
48
+ }
49
+ this.initialized = false;
50
+ }
51
+ isInitialized() {
52
+ return this.initialized;
53
+ }
54
+ ensureInitialized() {
55
+ if (!this.initialized) {
56
+ throw new Error('ForgeFeatureFlags not initialized. Call initialize() first.');
57
+ }
58
+ }
59
+ convertUser(user) {
60
+ return {
61
+ userID: user.userID,
62
+ custom: user.custom
63
+ };
64
+ }
65
+ }
66
+ exports.ForgeFeatureFlags = ForgeFeatureFlags;
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@forge/feature-flags-node",
3
+ "version": "1.0.0-next.0",
4
+ "description": "Feature Flags Node SDK for Atlassian Forge apps running on Node Runtime",
5
+ "author": "Atlassian",
6
+ "license": "SEE LICENSE IN LICENSE.txt",
7
+ "main": "out/index.js",
8
+ "types": "out/index.d.ts",
9
+ "scripts": {
10
+ "build": "yarn run clean && yarn run compile",
11
+ "clean": "rm -rf ./out && rm -f tsconfig.tsbuildinfo",
12
+ "compile": "tsc -b -v"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "20.19.1"
16
+ },
17
+ "dependencies": {
18
+ "@forge/api": "^6.0.2-next.3",
19
+ "statsig-node": "^6.4.3"
20
+ },
21
+ "publishConfig": {
22
+ "registry": "https://packages.atlassian.com/api/npm/npm-public/"
23
+ }
24
+ }