diff --git a/.github/workflows/fastly.yml b/.github/workflows/fastly.yml
new file mode 100644
index 0000000000..51939efb3f
--- /dev/null
+++ b/.github/workflows/fastly.yml
@@ -0,0 +1,24 @@
+name: sdk/fastly
+
+on:
+ push:
+ branches: [main, 'feat/**']
+ paths-ignore:
+ - '**.md' #Do not need to run CI for markdown changes.
+ pull_request:
+ branches: [main, 'feat/**']
+ paths-ignore:
+ - '**.md'
+
+jobs:
+ build-test-fastly:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ - id: shared
+ name: Shared CI Steps
+ uses: ./actions/ci
+ with:
+ workspace_name: '@launchdarkly/fastly-server-sdk'
+ workspace_path: packages/sdk/fastly
diff --git a/.github/workflows/manual-publish-docs.yml b/.github/workflows/manual-publish-docs.yml
index 907b270001..617664c430 100644
--- a/.github/workflows/manual-publish-docs.yml
+++ b/.github/workflows/manual-publish-docs.yml
@@ -12,6 +12,7 @@ on:
- packages/shared/sdk-server-edge
- packages/shared/akamai-edgeworker-sdk
- packages/sdk/cloudflare
+ - packages/sdk/fastly
- packages/sdk/server-node
- packages/sdk/vercel
- packages/sdk/akamai-base
diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml
index c1ca69e3b3..46ef1d4e16 100644
--- a/.github/workflows/manual-publish.yml
+++ b/.github/workflows/manual-publish.yml
@@ -22,6 +22,7 @@ on:
- packages/shared/sdk-server-edge
- packages/shared/akamai-edgeworker-sdk
- packages/sdk/cloudflare
+ - packages/sdk/fastly
- packages/sdk/react-native
- packages/sdk/server-node
- packages/sdk/react-universal
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 33b7a55f1f..dd597d7d21 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -14,6 +14,7 @@ jobs:
package-sdk-server-edge-released: ${{ steps.release.outputs['packages/shared/sdk-server-edge--release_created'] }}
package-akamai-edgeworker-sdk-released: ${{ steps.release.outputs['packages/shared/akamai-edgeworker-sdk--release_created'] }}
package-cloudflare-released: ${{ steps.release.outputs['packages/sdk/cloudflare--release_created'] }}
+ package-fastly-released: ${{ steps.release.outputs['packages/sdk/fastly--release_created'] }}
package-react-native-released: ${{ steps.release.outputs['packages/sdk/react-native--release_created'] }}
package-server-node-released: ${{ steps.release.outputs['packages/sdk/server-node--release_created'] }}
package-vercel-released: ${{ steps.release.outputs['packages/sdk/vercel--release_created'] }}
@@ -152,6 +153,26 @@ jobs:
workspace_path: packages/sdk/cloudflare
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
+ release-fastly:
+ runs-on: ubuntu-latest
+ needs: ['release-please', 'release-sdk-server']
+ permissions:
+ id-token: write
+ contents: write
+ if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-fastly-released == 'true'}}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20.x
+ registry-url: 'https://registry.npmjs.org'
+ - id: release-fastly
+ name: Full release of packages/sdk/fastly
+ uses: ./actions/full-release
+ with:
+ workspace_path: packages/sdk/fastly
+ aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
+
release-react-native:
runs-on: ubuntu-latest
needs: ['release-please', 'release-sdk-client']
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ee285fe746..9cb2c11d68 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -3,6 +3,7 @@
"packages/shared/sdk-server": "2.10.0",
"packages/sdk/server-node": "9.7.2",
"packages/sdk/cloudflare": "2.6.3",
+ "packages/sdk/fastly": "0.0.1",
"packages/shared/sdk-server-edge": "2.5.2",
"packages/sdk/vercel": "1.3.21",
"packages/sdk/akamai-base": "2.1.20",
diff --git a/.sdk_metadata.json b/.sdk_metadata.json
index fe6500fadc..462a1cf7c0 100644
--- a/.sdk_metadata.json
+++ b/.sdk_metadata.json
@@ -28,6 +28,15 @@
"tag-prefix": "cloudflare-server-sdk-"
}
},
+ "Fastly": {
+ "name": "Fastly SDK",
+ "type": "edge",
+ "path": "packages/sdk/fastly",
+ "languages": ["JavaScript", "TypeScript"],
+ "releases": {
+ "tag-prefix": "faslty-server-sdk-"
+ }
+ },
"react-native": {
"name": "React Native SDK",
"type": "client-side",
diff --git a/package.json b/package.json
index b7b0bdc24b..65d85b770b 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
"packages/sdk/server-node",
"packages/sdk/cloudflare",
"packages/sdk/cloudflare/example",
+ "packages/sdk/fastly",
+ "packages/sdk/fastly/example",
"packages/sdk/react-native",
"packages/sdk/react-native/example",
"packages/sdk/react-universal",
diff --git a/packages/sdk/fastly/README.md b/packages/sdk/fastly/README.md
new file mode 100644
index 0000000000..0f14072ade
--- /dev/null
+++ b/packages/sdk/fastly/README.md
@@ -0,0 +1,62 @@
+# LaunchDarkly SDK for Fastly
+
+The LaunchDarkly SDK for Fastly is designed primarily for use in [Fastly Compute Platform](https://www.fastly.com/documentation/guides/compute/). It follows the server-side LaunchDarkly model for multi-user contexts. It is not intended for use in desktop and embedded systems applications.
+
+# ⛔️⛔️⛔️⛔️
+
+> [!CAUTION]
+> This library is an alpha version and should not be considered ready for production use while this message is visible.
+
+# ☝️☝️☝️☝️☝️☝️
+
+## Install
+
+```shell
+# npm
+npm i @launchdarkly/fastly-server-sdk
+
+# yarn
+yarn add @launchdarkly/fastly-server-sdk
+```
+
+## Quickstart
+
+See the full [example app](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/fastly/example).
+
+## Developing this SDK
+
+```shell
+# at js-core repo root
+yarn && yarn build && cd packages/sdk/fastly
+
+# run tests
+yarn test
+```
+
+## Verifying SDK build provenance with the SLSA framework
+
+LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md).
+
+## About LaunchDarkly
+
+- LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
+ - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
+ - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
+ - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
+ - Grant access to certain features based on user attributes, like payment plan (eg: users on the 'gold' plan get access to more features than users in the 'silver' plan).
+ - Disable parts of your application to facilitate maintenance, without taking everything offline.
+- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
+- Explore LaunchDarkly
+ - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information
+ - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides
+ - [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation
+ - [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates
+
+[sdk-fastly-ci-badge]: https://github.com/launchdarkly/js-core/actions/workflows/fastly.yml/badge.svg
+[sdk-fastly-ci]: https://github.com/launchdarkly/js-core/actions/workflows/fastly.yml
+[sdk-fastly-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/fastly-server-sdk.svg?style=flat-square
+[sdk-fastly-npm-link]: https://www.npmjs.com/package/@launchdarkly/fastly-server-sdk
+[sdk-fastly-ghp-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8
+[sdk-fastly-ghp-link]: https://launchdarkly.github.io/js-core/packages/sdk/fastly/docs/
+[sdk-fastly-dm-badge]: https://img.shields.io/npm/dm/@launchdarkly/fastly-server-sdk.svg?style=flat-square
+[sdk-fastly-dt-badge]: https://img.shields.io/npm/dt/@launchdarkly/fastly-server-sdk.svg?style=flat-square
diff --git a/packages/sdk/fastly/__mocks__/fastly:kv-store.ts b/packages/sdk/fastly/__mocks__/fastly:kv-store.ts
new file mode 100644
index 0000000000..583bcc1d7e
--- /dev/null
+++ b/packages/sdk/fastly/__mocks__/fastly:kv-store.ts
@@ -0,0 +1,8 @@
+export const KVStore = jest.fn().mockImplementation(() => ({
+ get: jest.fn(),
+ put: jest.fn(),
+ delete: jest.fn(),
+ getMulti: jest.fn(),
+ putMulti: jest.fn(),
+ deleteMulti: jest.fn(),
+}));
diff --git a/packages/sdk/fastly/__tests__/api/EdgeFeatureStore.test.ts b/packages/sdk/fastly/__tests__/api/EdgeFeatureStore.test.ts
new file mode 100644
index 0000000000..203d0bee06
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/api/EdgeFeatureStore.test.ts
@@ -0,0 +1,125 @@
+import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common';
+
+import { EdgeFeatureStore } from '../../src/api/EdgeFeatureStore';
+import mockEdgeProvider from '../../src/utils/mockEdgeProvider';
+import * as testData from './testData.json';
+
+describe('EdgeFeatureStore', () => {
+ const sdkKey = 'sdkKey';
+ const kvKey = `LD-Env-${sdkKey}`;
+ const mockLogger = {
+ error: jest.fn(),
+ warn: jest.fn(),
+ info: jest.fn(),
+ debug: jest.fn(),
+ };
+ const mockGet = mockEdgeProvider.get as jest.Mock;
+ let featureStore: LDFeatureStore;
+ let asyncFeatureStore: AsyncStoreFacade;
+
+ beforeEach(() => {
+ mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
+ featureStore = new EdgeFeatureStore(mockEdgeProvider, sdkKey, 'MockEdgeProvider', mockLogger);
+ asyncFeatureStore = new AsyncStoreFacade(featureStore);
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('get', () => {
+ test('get flag', async () => {
+ const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
+
+ expect(mockGet).toHaveBeenCalledWith(kvKey);
+ expect(flag).toMatchObject(testData.flags.testFlag1);
+ });
+
+ test('invalid flag key', async () => {
+ const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'invalid');
+
+ expect(flag).toBeUndefined();
+ });
+
+ test('get segment', async () => {
+ const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'testSegment1');
+
+ expect(mockGet).toHaveBeenCalledWith(kvKey);
+ expect(segment).toMatchObject(testData.segments.testSegment1);
+ });
+
+ test('invalid segment key', async () => {
+ const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'invalid');
+
+ expect(segment).toBeUndefined();
+ });
+
+ test('invalid kv key', async () => {
+ mockGet.mockImplementation(() => Promise.resolve(null));
+ const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
+
+ expect(flag).toBeNull();
+ });
+ });
+
+ describe('all', () => {
+ test('all flags', async () => {
+ const flags = await asyncFeatureStore.all({ namespace: 'features' });
+
+ expect(mockGet).toHaveBeenCalledWith(kvKey);
+ expect(flags).toMatchObject(testData.flags);
+ });
+
+ test('all segments', async () => {
+ const segment = await asyncFeatureStore.all({ namespace: 'segments' });
+
+ expect(mockGet).toHaveBeenCalledWith(kvKey);
+ expect(segment).toMatchObject(testData.segments);
+ });
+
+ test('invalid DataKind', async () => {
+ const flag = await asyncFeatureStore.all({ namespace: 'InvalidDataKind' });
+
+ expect(flag).toEqual({});
+ });
+
+ test('invalid kv key', async () => {
+ mockGet.mockImplementation(() => Promise.resolve(null));
+ const segment = await asyncFeatureStore.all({ namespace: 'segments' });
+
+ expect(segment).toEqual({});
+ });
+ });
+
+ describe('initialized', () => {
+ test('is initialized', async () => {
+ const isInitialized = await asyncFeatureStore.initialized();
+
+ expect(mockGet).toHaveBeenCalledWith(kvKey);
+ expect(isInitialized).toBeTruthy();
+ });
+
+ test('not initialized', async () => {
+ mockGet.mockImplementation(() => Promise.resolve(null));
+ const isInitialized = await asyncFeatureStore.initialized();
+
+ expect(mockGet).toHaveBeenCalledWith(kvKey);
+ expect(isInitialized).toBeFalsy();
+ });
+ });
+
+ describe('init & getDescription', () => {
+ test('init', (done) => {
+ const cb = jest.fn(() => {
+ done();
+ });
+ featureStore.init(testData, cb);
+ });
+
+ test('getDescription', async () => {
+ const description = featureStore.getDescription?.();
+
+ expect(description).toEqual('MockEdgeProvider');
+ });
+ });
+});
diff --git a/packages/sdk/fastly/__tests__/api/LDClient.test.ts b/packages/sdk/fastly/__tests__/api/LDClient.test.ts
new file mode 100644
index 0000000000..cc5488e67f
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/api/LDClient.test.ts
@@ -0,0 +1,46 @@
+import { internal } from '@launchdarkly/js-server-sdk-common';
+
+import LDClient from '../../src/api/LDClient';
+import { createBasicPlatform } from '../createBasicPlatform';
+
+jest.mock('@launchdarkly/js-sdk-common', () => {
+ const actual = jest.requireActual('@launchdarkly/js-sdk-common');
+ return {
+ ...actual,
+ ...{
+ internal: {
+ ...actual.internal,
+ DiagnosticsManager: jest.fn(),
+ EventProcessor: jest.fn(),
+ },
+ },
+ };
+});
+
+const mockEventProcessor = internal.EventProcessor as jest.Mock;
+describe('Edge LDClient', () => {
+ it('uses clientSideID endpoints', async () => {
+ const client = new LDClient(
+ 'client-side-id',
+ createBasicPlatform().info,
+ {
+ sendEvents: true,
+ },
+ 'launchdarkly',
+ );
+ await client.waitForInitialization({ timeout: 10 });
+ const passedConfig = mockEventProcessor.mock.calls[0][0];
+
+ expect(passedConfig).toMatchObject({
+ sendEvents: true,
+ serviceEndpoints: {
+ includeAuthorizationHeader: false,
+ analyticsEventPath: '/events/bulk/client-side-id',
+ diagnosticEventPath: '/events/diagnostic/client-side-id',
+ events: 'https://events.launchdarkly.com',
+ polling: 'https://sdk.launchdarkly.com',
+ streaming: 'https://stream.launchdarkly.com',
+ },
+ });
+ });
+});
diff --git a/packages/sdk/fastly/__tests__/api/createOptions.test.ts b/packages/sdk/fastly/__tests__/api/createOptions.test.ts
new file mode 100644
index 0000000000..02fcc4b45d
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/api/createOptions.test.ts
@@ -0,0 +1,17 @@
+import { BasicLogger } from '@launchdarkly/js-server-sdk-common';
+
+import createOptions, { defaultOptions } from '../../src/api/createOptions';
+
+describe('createOptions', () => {
+ test('default options', () => {
+ expect(createOptions({})).toEqual(defaultOptions);
+ });
+
+ test('override logger', () => {
+ const logger = new BasicLogger({ name: 'test' });
+ expect(createOptions({ logger })).toEqual({
+ ...defaultOptions,
+ logger,
+ });
+ });
+});
diff --git a/packages/sdk/fastly/__tests__/api/testData.json b/packages/sdk/fastly/__tests__/api/testData.json
new file mode 100644
index 0000000000..b9e5296c03
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/api/testData.json
@@ -0,0 +1,171 @@
+{
+ "flags": {
+ "testFlag1": {
+ "key": "testFlag1",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "rules": [
+ {
+ "variation": 1,
+ "id": "rule1",
+ "clauses": [
+ {
+ "contextKind": "user",
+ "attribute": "/email",
+ "op": "contains",
+ "values": ["gmail"],
+ "negate": false
+ }
+ ],
+ "trackEvents": false,
+ "rollout": {
+ "bucketBy": "bucket",
+ "variations": [{ "variation": 1, "weight": 100 }]
+ }
+ }
+ ],
+ "fallthrough": {
+ "variation": 0
+ },
+ "offVariation": 1,
+ "variations": [true, false],
+ "clientSideAvailability": {
+ "usingMobileKey": true,
+ "usingEnvironmentId": true
+ },
+ "clientSide": true,
+ "salt": "aef830243d6640d0a973be89988e008d",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": 2000,
+ "version": 2,
+ "deleted": false
+ },
+ "testFlag2": {
+ "key": "testFlag2",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "rules": [],
+ "fallthrough": {
+ "variation": 0,
+ "rollout": {
+ "bucketBy": "bucket",
+ "variations": [{ "variation": 1, "weight": 100 }],
+ "contextKind:": "user",
+ "attribute": "/email"
+ }
+ },
+ "offVariation": 1,
+ "variations": [true, false],
+ "clientSideAvailability": {
+ "usingMobileKey": true,
+ "usingEnvironmentId": true
+ },
+ "clientSide": true,
+ "salt": "aef830243d6640d0a973be89988e008d",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": 2000,
+ "version": 2,
+ "deleted": false
+ },
+ "testFlag3": {
+ "key": "testFlag3",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "rules": [
+ {
+ "variation": 1,
+ "id": "rule1",
+ "clauses": [
+ {
+ "op": "segmentMatch",
+ "values": ["testSegment1"],
+ "negate": false
+ }
+ ],
+ "trackEvents": false
+ }
+ ],
+ "fallthrough": {
+ "variation": 0
+ },
+ "offVariation": 1,
+ "variations": [true, false],
+ "clientSideAvailability": {
+ "usingMobileKey": true,
+ "usingEnvironmentId": true
+ },
+ "clientSide": true,
+ "salt": "aef830243d6640d0a973be89988e008d",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": 2000,
+ "version": 2,
+ "deleted": false
+ }
+ },
+ "segments": {
+ "testSegment1": {
+ "name": "testSegment1",
+ "tags": [],
+ "creationDate": 1676063792158,
+ "key": "testSegment1",
+ "included": [],
+ "excluded": [],
+ "includedContexts": [],
+ "excludedContexts": [],
+ "_links": {
+ "parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
+ "self": {
+ "href": "/api/v2/segments/default/test/beta-users-1",
+ "type": "application/json"
+ },
+ "site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
+ },
+ "rules": [
+ {
+ "id": "rule-country",
+ "clauses": [
+ {
+ "attribute": "country",
+ "op": "in",
+ "values": ["australia"],
+ "negate": false
+ }
+ ]
+ }
+ ],
+ "version": 1,
+ "deleted": false,
+ "_access": { "denied": [], "allowed": [] },
+ "generation": 1
+ },
+ "testSegment2": {
+ "name": "testSegment2",
+ "tags": [],
+ "creationDate": 1676063792158,
+ "key": "testSegment2",
+ "included": [],
+ "excluded": [],
+ "includedContexts": [],
+ "excludedContexts": [],
+ "_links": {
+ "parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
+ "self": {
+ "href": "/api/v2/segments/default/test/beta-users-1",
+ "type": "application/json"
+ },
+ "site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
+ },
+ "rules": [],
+ "version": 1,
+ "deleted": false,
+ "_access": { "denied": [], "allowed": [] },
+ "generation": 1
+ }
+ }
+}
diff --git a/packages/sdk/fastly/__tests__/createBasicPlatform.ts b/packages/sdk/fastly/__tests__/createBasicPlatform.ts
new file mode 100644
index 0000000000..e5139ccec6
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/createBasicPlatform.ts
@@ -0,0 +1,59 @@
+import { PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common';
+
+import { setupCrypto } from './setupCrypto';
+
+const setupInfo = () => ({
+ platformData: jest.fn(
+ (): PlatformData => ({
+ os: {
+ name: 'An OS',
+ version: '1.0.1',
+ arch: 'An Arch',
+ },
+ name: 'The SDK Name',
+ additional: {
+ nodeVersion: '42',
+ },
+ ld_application: {
+ key: '',
+ envAttributesVersion: '1.0',
+ id: 'com.testapp.ld',
+ name: 'LDApplication.TestApp',
+ version: '1.1.1',
+ },
+ ld_device: {
+ key: '',
+ envAttributesVersion: '1.0',
+ os: { name: 'Another OS', version: '99', family: 'orange' },
+ manufacturer: 'coconut',
+ },
+ }),
+ ),
+ sdkData: jest.fn(
+ (): SdkData => ({
+ name: 'An SDK',
+ version: '2.0.2',
+ userAgentBase: 'TestUserAgent',
+ wrapperName: 'Rapper',
+ wrapperVersion: '1.2.3',
+ }),
+ ),
+});
+
+export const createBasicPlatform = () => ({
+ encoding: {
+ btoa: (s: string) => Buffer.from(s).toString('base64'),
+ },
+ info: setupInfo(),
+ crypto: setupCrypto(),
+ requests: {
+ fetch: jest.fn(),
+ createEventSource: jest.fn(),
+ getEventSourceCapabilities: jest.fn(),
+ },
+ storage: {
+ get: jest.fn(),
+ set: jest.fn(),
+ clear: jest.fn(),
+ },
+});
diff --git a/packages/sdk/fastly/__tests__/createPlatformInfo.test.ts b/packages/sdk/fastly/__tests__/createPlatformInfo.test.ts
new file mode 100644
index 0000000000..8698a53ac3
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/createPlatformInfo.test.ts
@@ -0,0 +1,17 @@
+import createPlatformInfo from '../src/createPlatformInfo';
+
+describe('Fastly Platform Info', () => {
+ it('platformData shows correct information', () => {
+ const platformData = createPlatformInfo();
+
+ expect(platformData.platformData()).toEqual({
+ name: 'Fastly Edge',
+ });
+
+ expect(platformData.sdkData()).toEqual({
+ name: '@launchdarkly/fastly-server-sdk',
+ version: '__LD_VERSION__',
+ userAgentBase: 'FastlyEdgeSDK',
+ });
+ });
+});
diff --git a/packages/sdk/fastly/__tests__/index.test.ts b/packages/sdk/fastly/__tests__/index.test.ts
new file mode 100644
index 0000000000..bd3debb37d
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/index.test.ts
@@ -0,0 +1,110 @@
+///
+import { KVStore } from 'fastly:kv-store';
+
+import { LDClient } from '../src/api';
+import { init } from '../src/index';
+import * as testData from './utils/testData.json';
+
+// Tell Jest to use the manual mock
+jest.mock('fastly:kv-store');
+
+const sdkKey = 'test-sdk-key';
+const flagKey1 = 'testFlag1';
+const flagKey2 = 'testFlag2';
+const flagKey3 = 'testFlag3';
+const context = { kind: 'user', key: 'test-user-key-1' };
+
+describe('init', () => {
+ let ldClient: LDClient;
+ let mockKVStore: jest.Mocked;
+
+ beforeAll(async () => {
+ mockKVStore = new KVStore('test-kv-store') as jest.Mocked;
+ const testDataString = JSON.stringify(testData);
+ mockKVStore.get.mockResolvedValue({
+ text: jest.fn().mockResolvedValue(testDataString),
+ json: jest.fn().mockResolvedValue(testData),
+ arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)),
+ body: new ReadableStream(),
+ bodyUsed: false,
+ metadata: () => null,
+ metadataText: () => null,
+ });
+ ldClient = init(sdkKey, mockKVStore);
+ await ldClient.waitForInitialization();
+ });
+
+ afterAll(() => {
+ ldClient.close();
+ });
+
+ describe('flags', () => {
+ test('variation default', async () => {
+ const value = await ldClient.variation(flagKey1, context, false);
+ expect(value).toBeTruthy();
+ });
+
+ test('variation default rollout', async () => {
+ const contextWithEmail = { ...context, email: 'test@yahoo.com' };
+ const value = await ldClient.variation(flagKey2, contextWithEmail, false);
+ const detail = await ldClient.variationDetail(flagKey2, contextWithEmail, false);
+
+ expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 });
+ expect(value).toBeTruthy();
+ });
+
+ test('rule match', async () => {
+ const contextWithEmail = { ...context, email: 'test@gmail.com' };
+ const value = await ldClient.variation(flagKey1, contextWithEmail, false);
+ const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false);
+
+ expect(detail).toEqual({
+ reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 },
+ value: false,
+ variationIndex: 1,
+ });
+ expect(value).toBeFalsy();
+ });
+
+ test('fallthrough', async () => {
+ const contextWithEmail = { ...context, email: 'test@yahoo.com' };
+ const value = await ldClient.variation(flagKey1, contextWithEmail, false);
+ const detail = await ldClient.variationDetail(flagKey1, contextWithEmail, false);
+
+ expect(detail).toEqual({ reason: { kind: 'FALLTHROUGH' }, value: true, variationIndex: 0 });
+ expect(value).toBeTruthy();
+ });
+
+ test('allFlags fallthrough', async () => {
+ const allFlags = await ldClient.allFlagsState(context);
+
+ expect(allFlags).toBeDefined();
+ expect(allFlags.toJSON()).toEqual({
+ $flagsState: {
+ testFlag1: { debugEventsUntilDate: 2000, variation: 0, version: 2 },
+ testFlag2: { debugEventsUntilDate: 2000, variation: 0, version: 2 },
+ testFlag3: { debugEventsUntilDate: 2000, variation: 0, version: 2 },
+ },
+ $valid: true,
+ testFlag1: true,
+ testFlag2: true,
+ testFlag3: true,
+ });
+ });
+ });
+
+ describe('segments', () => {
+ test('segment by country', async () => {
+ const contextWithCountry = { ...context, country: 'australia' };
+ const value = await ldClient.variation(flagKey3, contextWithCountry, false);
+ const detail = await ldClient.variationDetail(flagKey3, contextWithCountry, false);
+
+ expect(detail).toEqual({
+ reason: { kind: 'RULE_MATCH', ruleId: 'rule1', ruleIndex: 0 },
+ value: false,
+ variationIndex: 1,
+ });
+ expect(value).toBeFalsy();
+ });
+ });
+});
diff --git a/packages/sdk/fastly/__tests__/setupCrypto.ts b/packages/sdk/fastly/__tests__/setupCrypto.ts
new file mode 100644
index 0000000000..bdf62024fc
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/setupCrypto.ts
@@ -0,0 +1,20 @@
+import { Hasher } from '@launchdarkly/js-server-sdk-common';
+
+export const setupCrypto = () => {
+ let counter = 0;
+ const hasher = {
+ update: jest.fn((): Hasher => hasher),
+ digest: jest.fn(() => '1234567890123456'),
+ };
+
+ return {
+ createHash: jest.fn(() => hasher),
+ createHmac: jest.fn(),
+ randomUUID: jest.fn(() => {
+ counter += 1;
+ // Will provide a unique value for tests.
+ // Very much not a UUID of course.
+ return `${counter}`;
+ }),
+ };
+};
diff --git a/packages/sdk/fastly/__tests__/utils/testData.json b/packages/sdk/fastly/__tests__/utils/testData.json
new file mode 100644
index 0000000000..b9e5296c03
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/utils/testData.json
@@ -0,0 +1,171 @@
+{
+ "flags": {
+ "testFlag1": {
+ "key": "testFlag1",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "rules": [
+ {
+ "variation": 1,
+ "id": "rule1",
+ "clauses": [
+ {
+ "contextKind": "user",
+ "attribute": "/email",
+ "op": "contains",
+ "values": ["gmail"],
+ "negate": false
+ }
+ ],
+ "trackEvents": false,
+ "rollout": {
+ "bucketBy": "bucket",
+ "variations": [{ "variation": 1, "weight": 100 }]
+ }
+ }
+ ],
+ "fallthrough": {
+ "variation": 0
+ },
+ "offVariation": 1,
+ "variations": [true, false],
+ "clientSideAvailability": {
+ "usingMobileKey": true,
+ "usingEnvironmentId": true
+ },
+ "clientSide": true,
+ "salt": "aef830243d6640d0a973be89988e008d",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": 2000,
+ "version": 2,
+ "deleted": false
+ },
+ "testFlag2": {
+ "key": "testFlag2",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "rules": [],
+ "fallthrough": {
+ "variation": 0,
+ "rollout": {
+ "bucketBy": "bucket",
+ "variations": [{ "variation": 1, "weight": 100 }],
+ "contextKind:": "user",
+ "attribute": "/email"
+ }
+ },
+ "offVariation": 1,
+ "variations": [true, false],
+ "clientSideAvailability": {
+ "usingMobileKey": true,
+ "usingEnvironmentId": true
+ },
+ "clientSide": true,
+ "salt": "aef830243d6640d0a973be89988e008d",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": 2000,
+ "version": 2,
+ "deleted": false
+ },
+ "testFlag3": {
+ "key": "testFlag3",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "rules": [
+ {
+ "variation": 1,
+ "id": "rule1",
+ "clauses": [
+ {
+ "op": "segmentMatch",
+ "values": ["testSegment1"],
+ "negate": false
+ }
+ ],
+ "trackEvents": false
+ }
+ ],
+ "fallthrough": {
+ "variation": 0
+ },
+ "offVariation": 1,
+ "variations": [true, false],
+ "clientSideAvailability": {
+ "usingMobileKey": true,
+ "usingEnvironmentId": true
+ },
+ "clientSide": true,
+ "salt": "aef830243d6640d0a973be89988e008d",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": 2000,
+ "version": 2,
+ "deleted": false
+ }
+ },
+ "segments": {
+ "testSegment1": {
+ "name": "testSegment1",
+ "tags": [],
+ "creationDate": 1676063792158,
+ "key": "testSegment1",
+ "included": [],
+ "excluded": [],
+ "includedContexts": [],
+ "excludedContexts": [],
+ "_links": {
+ "parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
+ "self": {
+ "href": "/api/v2/segments/default/test/beta-users-1",
+ "type": "application/json"
+ },
+ "site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
+ },
+ "rules": [
+ {
+ "id": "rule-country",
+ "clauses": [
+ {
+ "attribute": "country",
+ "op": "in",
+ "values": ["australia"],
+ "negate": false
+ }
+ ]
+ }
+ ],
+ "version": 1,
+ "deleted": false,
+ "_access": { "denied": [], "allowed": [] },
+ "generation": 1
+ },
+ "testSegment2": {
+ "name": "testSegment2",
+ "tags": [],
+ "creationDate": 1676063792158,
+ "key": "testSegment2",
+ "included": [],
+ "excluded": [],
+ "includedContexts": [],
+ "excludedContexts": [],
+ "_links": {
+ "parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
+ "self": {
+ "href": "/api/v2/segments/default/test/beta-users-1",
+ "type": "application/json"
+ },
+ "site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
+ },
+ "rules": [],
+ "version": 1,
+ "deleted": false,
+ "_access": { "denied": [], "allowed": [] },
+ "generation": 1
+ }
+ }
+}
diff --git a/packages/sdk/fastly/__tests__/utils/validateOptions.test.ts b/packages/sdk/fastly/__tests__/utils/validateOptions.test.ts
new file mode 100644
index 0000000000..3b5826352e
--- /dev/null
+++ b/packages/sdk/fastly/__tests__/utils/validateOptions.test.ts
@@ -0,0 +1,46 @@
+import { BasicLogger } from '@launchdarkly/js-server-sdk-common';
+
+import mockFeatureStore from '../../src/utils/mockFeatureStore';
+import validateOptions from '../../src/utils/validateOptions';
+
+describe('validateOptions', () => {
+ test('throws without SDK key', () => {
+ expect(() => {
+ validateOptions('', {});
+ }).toThrowError(/You must configure the client with a client key/);
+ });
+
+ test('throws without featureStore', () => {
+ expect(() => {
+ validateOptions('test-sdk-key', {});
+ }).toThrowError(/You must configure the client with a feature store/);
+ });
+
+ test('throws without logger', () => {
+ expect(() => {
+ validateOptions('test-sdk-key', { featureStore: mockFeatureStore });
+ }).toThrowError(/You must configure the client with a logger/);
+ });
+
+ test('success valid options', () => {
+ expect(
+ validateOptions('test-sdk-key', {
+ featureStore: mockFeatureStore,
+ logger: BasicLogger.get(),
+ sendEvents: false,
+ }),
+ ).toBeTruthy();
+ });
+
+ test('throws with invalid options', () => {
+ expect(() => {
+ validateOptions('test-sdk-key', {
+ featureStore: mockFeatureStore,
+ logger: BasicLogger.get(),
+ // @ts-ignore
+ streamUri: 'invalid-option',
+ proxyOptions: 'another-invalid-option',
+ });
+ }).toThrow(/Invalid configuration: streamUri,proxyOptions not supported/);
+ });
+});
diff --git a/packages/sdk/fastly/example/.gitignore b/packages/sdk/fastly/example/.gitignore
new file mode 100644
index 0000000000..ab78770b99
--- /dev/null
+++ b/packages/sdk/fastly/example/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/bin
+/build
+/pkg
+.env
diff --git a/packages/sdk/fastly/example/README.md b/packages/sdk/fastly/example/README.md
new file mode 100644
index 0000000000..5b26a65699
--- /dev/null
+++ b/packages/sdk/fastly/example/README.md
@@ -0,0 +1,70 @@
+# Example test app for Fastly LaunchDarkly SDK
+
+This is an example test app to showcase the usage of the Fastly LaunchDarkly SDK in a [Fastly Compute@Edge](https://docs.fastly.com/products/compute-at-edge) application. The example demonstrates:
+
+1. Initializing the LaunchDarkly SDK with a Fastly KV Store
+2. Evaluating boolean and string feature flags
+3. Using multi-kind contexts to include Fastly-specific data
+4. Serving different images based on feature flag variations
+
+Most of the LaunchDarkly-related code can be found in [src/index.ts](src/index.ts).
+
+## Prerequisites
+
+A node environment of version 16 and yarn are required to develop in this repository.
+You will also need the [Fastly CLI](https://developer.fastly.com/learning/tools/cli) installed and a Fastly account to setup
+the test data required by this example.
+
+## Setting up your LaunchDarkly environment
+
+For simplicity, we recommend [creating a new LaunchDarkly project](https://docs.launchdarkly.com/home/organize/projects/?q=create+proj) for this example app. After creating a new project, create the following feature flags:
+
+- `example-flag` - (Boolean) - This flag is evaluated in the root endpoint
+- `animal` - (String) - This flag determines which animal image to show (values: "cat" or "dog")
+
+## Setting up your development environment
+
+1. At the root of the js-core repo:
+
+```shell
+yarn && yarn build
+```
+
+2. Replace `LAUNCHDARKLY_CLIENT_ID` in [src/index.ts](src/index.ts) with your LaunchDarkly SDK key.
+
+3. Create a new Fastly Compute service and KV Store:
+
+```shell
+# Login to Fastly CLI
+fastly login
+
+# Create a new Compute@Edge service
+fastly compute init
+
+# Create a new KV Store
+fastly kv-store create --name launchdarkly
+```
+
+4. Run the following command to install dependencies:
+
+```shell
+yarn
+```
+
+5. Start the local development server:
+
+```shell
+yarn start
+```
+
+6. Test the endpoints:
+
+- Visit `http://127.0.0.1:7676/` for the boolean flag evaluation
+- Visit `http://127.0.0.1:7676/animal` to see an image controlled by the string flag
+- Visit `http://127.0.0.1:7676/cat` or `http://127.0.0.1:7676/dog` for direct image access
+
+7. Deploy to Fastly:
+
+```shell
+yarn deploy
+```
diff --git a/packages/sdk/fastly/example/fastly.toml b/packages/sdk/fastly/example/fastly.toml
new file mode 100644
index 0000000000..21b8c30665
--- /dev/null
+++ b/packages/sdk/fastly/example/fastly.toml
@@ -0,0 +1,26 @@
+# This file describes a Fastly Compute package. To learn more visit:
+# https://www.fastly.com/documentation/reference/compute/fastly-toml
+
+authors = []
+description = "A basic example of using the LaunchDarkly SDK for Fastly"
+language = "javascript"
+manifest_version = 3
+name = "LaunchDarkly SDK for Fastly Example"
+service_id = ""
+
+[scripts]
+build = "yarn build"
+post_init = "yarn install"
+
+[local_server]
+
+[local_server.backends]
+
+[local_server.backends.launchdarkly]
+url = "https://events.launchdarkly.com"
+
+[local_server.kv_stores]
+
+[[local_server.kv_stores.launchdarkly_local]]
+key = "LD-Env-local"
+path = "./localData.json"
diff --git a/packages/sdk/fastly/example/localData.json b/packages/sdk/fastly/example/localData.json
new file mode 100644
index 0000000000..572030202b
--- /dev/null
+++ b/packages/sdk/fastly/example/localData.json
@@ -0,0 +1,67 @@
+{
+ "flags": {
+ "animal": {
+ "key": "animal",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "contextTargets": [],
+ "rules": [],
+ "fallthrough": {
+ "rollout": {
+ "contextKind": "fastly-request",
+ "variations": [
+ { "variation": 0, "weight": 50000 },
+ { "variation": 1, "weight": 50000 }
+ ],
+ "bucketBy": "key"
+ }
+ },
+ "offVariation": 1,
+ "variations": ["cat", "dog"],
+ "clientSideAvailability": { "usingMobileKey": false, "usingEnvironmentId": false },
+ "clientSide": false,
+ "salt": "0ab7b96471ff4edb98113157355cbb9f",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": null,
+ "version": 5,
+ "deleted": false
+ },
+ "example-flag": {
+ "key": "example-flag",
+ "on": true,
+ "prerequisites": [],
+ "targets": [],
+ "contextTargets": [],
+ "rules": [
+ {
+ "variation": 1,
+ "id": "8b96123e-759f-4f73-b91e-884ac56d0a06",
+ "clauses": [
+ {
+ "contextKind": "fastly-request",
+ "attribute": "fastly_region",
+ "op": "in",
+ "values": ["US-West"],
+ "negate": false
+ }
+ ],
+ "trackEvents": false
+ }
+ ],
+ "fallthrough": { "variation": 0 },
+ "offVariation": 1,
+ "variations": [true, false],
+ "clientSideAvailability": { "usingMobileKey": false, "usingEnvironmentId": false },
+ "clientSide": false,
+ "salt": "ca17f93252064631bacb2cffea217f20",
+ "trackEvents": false,
+ "trackEventsFallthrough": false,
+ "debugEventsUntilDate": null,
+ "version": 8,
+ "deleted": false
+ }
+ },
+ "segments": {}
+}
diff --git a/packages/sdk/fastly/example/package.json b/packages/sdk/fastly/example/package.json
new file mode 100644
index 0000000000..e7961c1d30
--- /dev/null
+++ b/packages/sdk/fastly/example/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "fastly-example",
+ "packageManager": "yarn@3.4.1",
+ "type": "module",
+ "engines": {
+ "node": "^16 || >=18"
+ },
+ "devDependencies": {
+ "@fastly/cli": "^10.17.1",
+ "rimraf": "^6.0.1",
+ "typescript": "^5.7.2"
+ },
+ "dependencies": {
+ "@fastly/js-compute": "^3.28.0",
+ "@launchdarkly/fastly-server-sdk": "workspace:^"
+ },
+ "scripts": {
+ "clean": "rimraf build && rimraf bin",
+ "build": "tsc && js-compute-runtime build/index.js bin/main.wasm",
+ "start": "fastly compute serve",
+ "deploy": "fastly compute publish"
+ }
+}
diff --git a/packages/sdk/fastly/example/src/cat.jpeg b/packages/sdk/fastly/example/src/cat.jpeg
new file mode 100644
index 0000000000..eaedfdb979
Binary files /dev/null and b/packages/sdk/fastly/example/src/cat.jpeg differ
diff --git a/packages/sdk/fastly/example/src/dog.jpeg b/packages/sdk/fastly/example/src/dog.jpeg
new file mode 100644
index 0000000000..c7b094d1a9
Binary files /dev/null and b/packages/sdk/fastly/example/src/dog.jpeg differ
diff --git a/packages/sdk/fastly/example/src/index.ts b/packages/sdk/fastly/example/src/index.ts
new file mode 100644
index 0000000000..5653ca08a2
--- /dev/null
+++ b/packages/sdk/fastly/example/src/index.ts
@@ -0,0 +1,118 @@
+/* eslint-disable no-console, @typescript-eslint/no-use-before-define, no-restricted-globals */
+///
+import { env } from 'fastly:env';
+import { includeBytes } from 'fastly:experimental';
+import { KVStore } from 'fastly:kv-store';
+
+import { init } from '@launchdarkly/fastly-server-sdk';
+import type { LDMultiKindContext } from '@launchdarkly/js-server-sdk-common';
+
+// Set your LaunchDarkly client ID here
+const LAUNCHDARKLY_CLIENT_ID = '';
+// Set the KV store name used to store the LaunchDarkly data here
+const KV_STORE_NAME = 'launchdarkly';
+// Set the Fastly Backend name used to send LaunchDarkly events here
+const EVENTS_BACKEND_NAME = 'launchdarkly';
+
+const cat = includeBytes('./src/cat.jpeg');
+const dog = includeBytes('./src/dog.jpeg');
+
+// The entry point for your application.
+//
+// Use this fetch event listener to define your main request handling logic. It
+// could be used to route based on the request properties (such as method or
+// path), send the request to a backend, make completely new requests, and/or
+// generate synthetic responses.
+
+addEventListener('fetch', (event) => event.respondWith(handleRequest(event)));
+
+async function handleRequest(event: FetchEvent) {
+ // Log service version
+ console.log('FASTLY_SERVICE_VERSION:', env('FASTLY_SERVICE_VERSION') || 'local');
+
+ // Get the client request.
+ const req = event.request;
+
+ // Filter requests that have unexpected methods.
+ if (!['HEAD', 'GET', 'PURGE'].includes(req.method)) {
+ return new Response('This method is not allowed', {
+ status: 405,
+ });
+ }
+
+ const isLocal = env('FASTLY_HOSTNAME') === 'localhost';
+ const kvStoreName = isLocal ? 'launchdarkly_local' : KV_STORE_NAME;
+ const ldClientId = isLocal ? 'local' : LAUNCHDARKLY_CLIENT_ID;
+
+ const store = new KVStore(kvStoreName);
+ const ldClient = init(ldClientId, store, {
+ sendEvents: true,
+ eventsBackendName: EVENTS_BACKEND_NAME,
+ });
+ await ldClient.waitForInitialization();
+
+ const flagContext: LDMultiKindContext = {
+ kind: 'multi',
+ user: {
+ // In a real-world scenario, you would use get the user key from a cookie, header, or other source
+ key: 'test-user',
+ },
+ 'fastly-request': {
+ key: env('FASTLY_TRACE_ID'),
+ fastly_service_version: env('FASTLY_SERVICE_VERSION'),
+ fastly_cache_generation: env('FASTLY_CACHE_GENERATION'),
+ fastly_hostname: env('FASTLY_HOSTNAME'),
+ fastly_pop: env('FASTLY_POP'),
+ fastly_region: env('FASTLY_REGION'),
+ fastly_service_id: env('FASTLY_SERVICE_ID'),
+ fastly_trace_id: env('FASTLY_TRACE_ID'),
+ },
+ };
+
+ const url = new URL(req.url);
+
+ if (url.pathname === '/') {
+ const flagKey = 'example-flag';
+ const variationDetail = await ldClient.boolVariationDetail(flagKey, flagContext, false);
+
+ const output = {
+ flagContext,
+ flagKey,
+ variationDetail,
+ };
+ event.waitUntil(ldClient.flush());
+
+ return new Response(JSON.stringify(output, undefined, 2), {
+ status: 200,
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ });
+ }
+
+ if (url.pathname === '/cat') {
+ return new Response(cat, {
+ status: 200,
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
+ });
+ }
+ if (url.pathname === '/dog') {
+ return new Response(dog, {
+ status: 200,
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
+ });
+ }
+ if (url.pathname === '/animal') {
+ const animal = await ldClient.stringVariation('animal', flagContext, 'cat');
+ const image = animal === 'cat' ? cat : dog;
+
+ event.waitUntil(ldClient.flush());
+ return new Response(image, {
+ status: 200,
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
+ });
+ }
+
+ // // Catch all other requests and return a 404.
+ return new Response('not found', {
+ status: 404,
+ });
+}
diff --git a/packages/sdk/fastly/example/tsconfig.json b/packages/sdk/fastly/example/tsconfig.json
new file mode 100644
index 0000000000..9f2c981cc9
--- /dev/null
+++ b/packages/sdk/fastly/example/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "module": "ES2022",
+ "target": "ES2022",
+ "moduleResolution": "bundler",
+ "customConditions": ["fastly"],
+ "esModuleInterop": true,
+ "lib": ["ES2022"],
+ "rootDir": "src",
+ "outDir": "build",
+ "types": ["@fastly/js-compute"],
+ "skipLibCheck": true
+ },
+ "include": ["./src/**/*.js", "./src/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/sdk/fastly/jest.config.json b/packages/sdk/fastly/jest.config.json
new file mode 100644
index 0000000000..6174807746
--- /dev/null
+++ b/packages/sdk/fastly/jest.config.json
@@ -0,0 +1,9 @@
+{
+ "transform": { "^.+\\.ts?$": "ts-jest" },
+ "testMatch": ["**/*.test.ts?(x)"],
+ "testPathIgnorePatterns": ["node_modules", "example", "dist"],
+ "modulePathIgnorePatterns": ["dist"],
+ "testEnvironment": "node",
+ "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
+ "collectCoverageFrom": ["src/**/*.ts"]
+}
diff --git a/packages/sdk/fastly/package.json b/packages/sdk/fastly/package.json
new file mode 100644
index 0000000000..02eb1dff75
--- /dev/null
+++ b/packages/sdk/fastly/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@launchdarkly/fastly-server-sdk",
+ "version": "0.0.1",
+ "packageManager": "yarn@3.4.1",
+ "description": "Cloudflare LaunchDarkly SDK",
+ "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/fastly",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/launchdarkly/js-core.git"
+ },
+ "license": "Apache-2.0",
+ "keywords": [
+ "launchdarkly",
+ "fastly",
+ "edge",
+ "compute",
+ "kv"
+ ],
+ "type": "module",
+ "exports": {
+ ".": {
+ "require": {
+ "types": "./dist/index.d.cts",
+ "require": "./dist/index.cjs"
+ },
+ "import": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ }
+ }
+ },
+ "main": "../dist/index.cjs",
+ "types": "./dist/index.d.cts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup && ../../../scripts/replace-version.sh .",
+ "clean": "rimraf dist",
+ "tsw": "yarn tsc --watch",
+ "start": "rimraf dist && yarn tsw",
+ "lint": "eslint . --ext .ts",
+ "test": "npx jest --runInBand",
+ "coverage": "yarn test --coverage",
+ "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore",
+ "check": "yarn prettier && yarn lint && yarn build && yarn test"
+ },
+ "dependencies": {
+ "@fastly/js-compute": "^3.28.0",
+ "@launchdarkly/js-server-sdk-common": "2.10.0",
+ "crypto-js": "^4.2.0"
+ },
+ "devDependencies": {
+ "@types/crypto-js": "^4.2.2",
+ "eslint": "^8.45.0",
+ "jest": "^29.7.0",
+ "prettier": "^3.4.2",
+ "rimraf": "^6.0.1",
+ "tsup": "^8.3.5",
+ "typedoc": "^0.27.4",
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/packages/sdk/fastly/src/api/EdgeFeatureStore.ts b/packages/sdk/fastly/src/api/EdgeFeatureStore.ts
new file mode 100644
index 0000000000..039ee4a300
--- /dev/null
+++ b/packages/sdk/fastly/src/api/EdgeFeatureStore.ts
@@ -0,0 +1,116 @@
+import type {
+ DataKind,
+ LDFeatureStore,
+ LDFeatureStoreDataStorage,
+ LDFeatureStoreItem,
+ LDFeatureStoreKindData,
+ LDLogger,
+} from '@launchdarkly/js-server-sdk-common';
+import { deserializePoll, noop } from '@launchdarkly/js-server-sdk-common';
+
+export interface EdgeProvider {
+ get: (rootKey: string) => Promise;
+}
+
+export class EdgeFeatureStore implements LDFeatureStore {
+ private readonly _rootKey: string;
+
+ constructor(
+ private readonly _edgeProvider: EdgeProvider,
+ sdkKey: string,
+ private readonly _description: string,
+ private _logger: LDLogger,
+ ) {
+ this._rootKey = `LD-Env-${sdkKey}`;
+ }
+
+ async get(
+ kind: DataKind,
+ dataKey: string,
+ callback: (res: LDFeatureStoreItem | null) => void,
+ ): Promise {
+ const { namespace } = kind;
+ const kindKey = namespace === 'features' ? 'flags' : namespace;
+ this._logger.debug(`Requesting ${dataKey} from ${this._rootKey}.${kindKey}`);
+
+ try {
+ const i = await this._edgeProvider.get(this._rootKey);
+
+ if (!i) {
+ throw new Error(`${this._rootKey}.${kindKey} is not found in KV.`);
+ }
+
+ const item = deserializePoll(i);
+ if (!item) {
+ throw new Error(`Error deserializing ${kindKey}`);
+ }
+
+ switch (namespace) {
+ case 'features':
+ callback(item.flags[dataKey]);
+ break;
+ case 'segments':
+ callback(item.segments[dataKey]);
+ break;
+ default:
+ callback(null);
+ }
+ } catch (err) {
+ this._logger.error(err);
+ callback(null);
+ }
+ }
+
+ async all(kind: DataKind, callback: (res: LDFeatureStoreKindData) => void = noop): Promise {
+ const { namespace } = kind;
+ const kindKey = namespace === 'features' ? 'flags' : namespace;
+ this._logger.debug(`Requesting all from ${this._rootKey}.${kindKey}`);
+ try {
+ const i = await this._edgeProvider.get(this._rootKey);
+ if (!i) {
+ throw new Error(`${this._rootKey}.${kindKey} is not found in KV.`);
+ }
+
+ const item = deserializePoll(i);
+ if (!item) {
+ throw new Error(`Error deserializing ${kindKey}`);
+ }
+
+ switch (namespace) {
+ case 'features':
+ callback(item.flags);
+ break;
+ case 'segments':
+ callback(item.segments);
+ break;
+ default:
+ callback({});
+ }
+ } catch (err) {
+ this._logger.error(err);
+ callback({});
+ }
+ }
+
+ async initialized(callback: (isInitialized: boolean) => void = noop): Promise {
+ const config = await this._edgeProvider.get(this._rootKey);
+ const result = config !== null;
+ this._logger.debug(`Is ${this._rootKey} initialized? ${result}`);
+ callback(result);
+ }
+
+ init(allData: LDFeatureStoreDataStorage, callback: () => void): void {
+ callback();
+ }
+
+ getDescription(): string {
+ return this._description;
+ }
+
+ // unused
+ close = noop;
+
+ delete = noop;
+
+ upsert = noop;
+}
diff --git a/packages/sdk/fastly/src/api/LDClient.ts b/packages/sdk/fastly/src/api/LDClient.ts
new file mode 100644
index 0000000000..b4bc5ce9e2
--- /dev/null
+++ b/packages/sdk/fastly/src/api/LDClient.ts
@@ -0,0 +1,24 @@
+import { Info, internal, LDClientImpl, LDOptions } from '@launchdarkly/js-server-sdk-common';
+
+import EdgePlatform from '../platform';
+import createCallbacks from './createCallbacks';
+import createOptions from './createOptions';
+
+/**
+ * The LaunchDarkly SDK edge client object.
+ */
+export default class LDClient extends LDClientImpl {
+ // clientSideID is only used to query the edge key-value store and send analytics, not to initialize with LD servers
+ constructor(clientSideID: string, platformInfo: Info, options: LDOptions, eventsBackend: string) {
+ const platform = new EdgePlatform(platformInfo, eventsBackend);
+ const internalOptions: internal.LDInternalOptions = {
+ analyticsEventPath: `/events/bulk/${clientSideID}`,
+ diagnosticEventPath: `/events/diagnostic/${clientSideID}`,
+ includeAuthorizationHeader: false,
+ };
+
+ const finalOptions = createOptions(options);
+
+ super(clientSideID, platform, finalOptions, createCallbacks(), internalOptions);
+ }
+}
diff --git a/packages/sdk/fastly/src/api/createCallbacks.ts b/packages/sdk/fastly/src/api/createCallbacks.ts
new file mode 100644
index 0000000000..922a4b9e08
--- /dev/null
+++ b/packages/sdk/fastly/src/api/createCallbacks.ts
@@ -0,0 +1,9 @@
+const createCallbacks = () => ({
+ onError: () => {},
+ onFailed: () => {},
+ onReady: () => {},
+ onUpdate: () => {},
+ hasEventListeners: () => false,
+});
+
+export default createCallbacks;
diff --git a/packages/sdk/fastly/src/api/createOptions.ts b/packages/sdk/fastly/src/api/createOptions.ts
new file mode 100644
index 0000000000..2999934e8d
--- /dev/null
+++ b/packages/sdk/fastly/src/api/createOptions.ts
@@ -0,0 +1,17 @@
+import { BasicLogger, LDOptions } from '@launchdarkly/js-server-sdk-common';
+
+export const defaultOptions: LDOptions = {
+ stream: false,
+ sendEvents: true,
+ useLdd: true,
+ diagnosticOptOut: true,
+ logger: BasicLogger.get(),
+};
+
+const createOptions = (options: LDOptions) => {
+ const finalOptions = { ...defaultOptions, ...options };
+ finalOptions.logger?.debug(`Using LD options: ${JSON.stringify(finalOptions)}`);
+ return finalOptions;
+};
+
+export default createOptions;
diff --git a/packages/sdk/fastly/src/api/index.ts b/packages/sdk/fastly/src/api/index.ts
new file mode 100644
index 0000000000..c4ae612f9d
--- /dev/null
+++ b/packages/sdk/fastly/src/api/index.ts
@@ -0,0 +1,4 @@
+import LDClient from './LDClient';
+
+export * from './EdgeFeatureStore';
+export { LDClient };
diff --git a/packages/sdk/fastly/src/createPlatformInfo.ts b/packages/sdk/fastly/src/createPlatformInfo.ts
new file mode 100644
index 0000000000..ee6fffffd6
--- /dev/null
+++ b/packages/sdk/fastly/src/createPlatformInfo.ts
@@ -0,0 +1,21 @@
+import { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common';
+
+class VercelPlatformInfo implements Info {
+ platformData(): PlatformData {
+ return {
+ name: 'Fastly Edge',
+ };
+ }
+
+ sdkData(): SdkData {
+ return {
+ name: '@launchdarkly/fastly-server-sdk',
+ version: '__LD_VERSION__',
+ userAgentBase: 'FastlyEdgeSDK',
+ };
+ }
+}
+
+const createPlatformInfo = () => new VercelPlatformInfo();
+
+export default createPlatformInfo;
diff --git a/packages/sdk/fastly/src/index.ts b/packages/sdk/fastly/src/index.ts
new file mode 100644
index 0000000000..5e4d14ae3e
--- /dev/null
+++ b/packages/sdk/fastly/src/index.ts
@@ -0,0 +1,48 @@
+///
+import { KVStore } from 'fastly:kv-store';
+
+import { BasicLogger, LDOptions } from '@launchdarkly/js-server-sdk-common';
+
+import { EdgeFeatureStore, EdgeProvider, LDClient } from './api';
+import createPlatformInfo from './createPlatformInfo';
+import validateOptions from './utils/validateOptions';
+
+const DEFAULT_EVENTS_BACKEND_NAME = 'launchdarkly';
+
+export type FastlySDKOptions = LDOptions & {
+ /**
+ * The Fastly Backend name to send LaunchDarkly events. Backends are configured using the Fastly service backend configuration. This option can be ignored if the `sendEvents` option is set to `false`. See [Fastly's Backend documentation](https://developer.fastly.com/reference/api/services/backend/) for more information. The default value is `launchdarkly`.
+ */
+ eventsBackendName?: string;
+};
+
+export const init = (
+ sdkKey: string,
+ kvStore: KVStore,
+ options: FastlySDKOptions = { eventsBackendName: DEFAULT_EVENTS_BACKEND_NAME },
+) => {
+ const logger = options.logger ?? BasicLogger.get();
+
+ const edgeProvider: EdgeProvider = {
+ get: async (rootKey: string) => {
+ const entry = await kvStore.get(rootKey);
+ return entry ? entry.text() : null;
+ },
+ };
+
+ const { eventsBackendName, ...ldOptions } = options;
+
+ const finalOptions = {
+ featureStore: new EdgeFeatureStore(edgeProvider, sdkKey, 'Fastly', logger),
+ logger,
+ ...ldOptions,
+ };
+
+ validateOptions(sdkKey, finalOptions);
+ return new LDClient(
+ sdkKey,
+ createPlatformInfo(),
+ finalOptions,
+ eventsBackendName || DEFAULT_EVENTS_BACKEND_NAME,
+ );
+};
diff --git a/packages/sdk/fastly/src/platform/crypto/cryptoJSHasher.ts b/packages/sdk/fastly/src/platform/crypto/cryptoJSHasher.ts
new file mode 100644
index 0000000000..4aec221057
--- /dev/null
+++ b/packages/sdk/fastly/src/platform/crypto/cryptoJSHasher.ts
@@ -0,0 +1,49 @@
+import CryptoJS from 'crypto-js';
+
+import { Hasher as LDHasher } from '@launchdarkly/js-server-sdk-common';
+
+import { SupportedHashAlgorithm, SupportedOutputEncoding } from './types';
+
+export default class CryptoJSHasher implements LDHasher {
+ private _cryptoJSHasher;
+
+ constructor(algorithm: SupportedHashAlgorithm) {
+ let algo;
+
+ switch (algorithm) {
+ case 'sha1':
+ algo = CryptoJS.algo.SHA1;
+ break;
+ case 'sha256':
+ algo = CryptoJS.algo.SHA256;
+ break;
+ default:
+ throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.');
+ }
+
+ this._cryptoJSHasher = algo.create();
+ }
+
+ digest(encoding: SupportedOutputEncoding): string {
+ const result = this._cryptoJSHasher.finalize();
+
+ let enc;
+ switch (encoding) {
+ case 'base64':
+ enc = CryptoJS.enc.Base64;
+ break;
+ case 'hex':
+ enc = CryptoJS.enc.Hex;
+ break;
+ default:
+ throw new Error('unsupported output encoding. Only base64 and hex are supported.');
+ }
+
+ return result.toString(enc);
+ }
+
+ update(data: string): this {
+ this._cryptoJSHasher.update(data);
+ return this;
+ }
+}
diff --git a/packages/sdk/fastly/src/platform/crypto/cryptoJSHmac.ts b/packages/sdk/fastly/src/platform/crypto/cryptoJSHmac.ts
new file mode 100644
index 0000000000..98e8976bb0
--- /dev/null
+++ b/packages/sdk/fastly/src/platform/crypto/cryptoJSHmac.ts
@@ -0,0 +1,45 @@
+import CryptoJS from 'crypto-js';
+
+import { Hmac as LDHmac } from '@launchdarkly/js-server-sdk-common';
+
+import { SupportedHashAlgorithm, SupportedOutputEncoding } from './types';
+
+export default class CryptoJSHmac implements LDHmac {
+ private _cryptoJSHmac;
+
+ constructor(algorithm: SupportedHashAlgorithm, key: string) {
+ let algo;
+
+ switch (algorithm) {
+ case 'sha1':
+ algo = CryptoJS.algo.SHA1;
+ break;
+ case 'sha256':
+ algo = CryptoJS.algo.SHA256;
+ break;
+ default:
+ throw new Error('unsupported hash algorithm. Only sha1 and sha256 are supported.');
+ }
+
+ this._cryptoJSHmac = CryptoJS.algo.HMAC.create(algo, key);
+ }
+
+ digest(encoding: SupportedOutputEncoding): string {
+ const result = this._cryptoJSHmac.finalize();
+
+ if (encoding === 'base64') {
+ return result.toString(CryptoJS.enc.Base64);
+ }
+
+ if (encoding === 'hex') {
+ return result.toString(CryptoJS.enc.Hex);
+ }
+
+ throw new Error('unsupported output encoding. Only base64 and hex are supported.');
+ }
+
+ update(data: string): this {
+ this._cryptoJSHmac.update(data);
+ return this;
+ }
+}
diff --git a/packages/sdk/fastly/src/platform/crypto/index.ts b/packages/sdk/fastly/src/platform/crypto/index.ts
new file mode 100644
index 0000000000..7a25f59036
--- /dev/null
+++ b/packages/sdk/fastly/src/platform/crypto/index.ts
@@ -0,0 +1,24 @@
+import type { Crypto, Hasher, Hmac } from '@launchdarkly/js-server-sdk-common';
+
+import CryptoJSHasher from './cryptoJSHasher';
+import CryptoJSHmac from './cryptoJSHmac';
+import { SupportedHashAlgorithm } from './types';
+
+/**
+ * Uses crypto-js as substitute to node:crypto because the latter
+ * is not yet supported in some runtimes.
+ * https://cryptojs.gitbook.io/docs/
+ */
+export default class EdgeCrypto implements Crypto {
+ createHash(algorithm: SupportedHashAlgorithm): Hasher {
+ return new CryptoJSHasher(algorithm);
+ }
+
+ createHmac(algorithm: SupportedHashAlgorithm, key: string): Hmac {
+ return new CryptoJSHmac(algorithm, key);
+ }
+
+ randomUUID(): string {
+ return crypto.randomUUID();
+ }
+}
diff --git a/packages/sdk/fastly/src/platform/crypto/types.ts b/packages/sdk/fastly/src/platform/crypto/types.ts
new file mode 100644
index 0000000000..3cf314d1f4
--- /dev/null
+++ b/packages/sdk/fastly/src/platform/crypto/types.ts
@@ -0,0 +1,2 @@
+export type SupportedHashAlgorithm = 'sha1' | 'sha256';
+export type SupportedOutputEncoding = 'base64' | 'hex';
diff --git a/packages/sdk/fastly/src/platform/index.ts b/packages/sdk/fastly/src/platform/index.ts
new file mode 100644
index 0000000000..e4d34ea977
--- /dev/null
+++ b/packages/sdk/fastly/src/platform/index.ts
@@ -0,0 +1,17 @@
+import type { Crypto, Info, Platform, Requests } from '@launchdarkly/js-server-sdk-common';
+
+import EdgeCrypto from './crypto';
+import EdgeRequests from './requests';
+
+export default class EdgePlatform implements Platform {
+ info: Info;
+
+ crypto: Crypto = new EdgeCrypto();
+
+ requests: Requests;
+
+ constructor(info: Info, eventsBackend: string) {
+ this.info = info;
+ this.requests = new EdgeRequests(eventsBackend);
+ }
+}
diff --git a/packages/sdk/fastly/src/platform/requests.ts b/packages/sdk/fastly/src/platform/requests.ts
new file mode 100644
index 0000000000..3ac39ae7cb
--- /dev/null
+++ b/packages/sdk/fastly/src/platform/requests.ts
@@ -0,0 +1,34 @@
+import { NullEventSource } from '@launchdarkly/js-server-sdk-common';
+import type {
+ EventSource,
+ EventSourceCapabilities,
+ EventSourceInitDict,
+ Options,
+ Requests,
+ Response,
+} from '@launchdarkly/js-server-sdk-common';
+
+export default class EdgeRequests implements Requests {
+ eventsBackend: string;
+
+ constructor(eventsBackend: string) {
+ this.eventsBackend = eventsBackend;
+ }
+
+ fetch(url: string, options: Options = {}): Promise {
+ // @ts-ignore
+ return fetch(url, { ...options, backend: this.eventsBackend });
+ }
+
+ createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource {
+ return new NullEventSource(url, eventSourceInitDict);
+ }
+
+ getEventSourceCapabilities(): EventSourceCapabilities {
+ return {
+ readTimeout: false,
+ headers: false,
+ customMethod: false,
+ };
+ }
+}
diff --git a/packages/sdk/fastly/src/utils/mockEdgeProvider.ts b/packages/sdk/fastly/src/utils/mockEdgeProvider.ts
new file mode 100644
index 0000000000..bdf8791af0
--- /dev/null
+++ b/packages/sdk/fastly/src/utils/mockEdgeProvider.ts
@@ -0,0 +1,7 @@
+import { EdgeProvider } from '../api';
+
+const mockEdgeProvider: EdgeProvider = {
+ get: jest.fn(),
+};
+
+export default mockEdgeProvider;
diff --git a/packages/sdk/fastly/src/utils/mockFeatureStore.ts b/packages/sdk/fastly/src/utils/mockFeatureStore.ts
new file mode 100644
index 0000000000..037bed69ec
--- /dev/null
+++ b/packages/sdk/fastly/src/utils/mockFeatureStore.ts
@@ -0,0 +1,13 @@
+import type { LDFeatureStore } from '@launchdarkly/js-server-sdk-common';
+
+const mockFeatureStore: LDFeatureStore = {
+ all: jest.fn(),
+ close: jest.fn(),
+ init: jest.fn(),
+ initialized: jest.fn(),
+ upsert: jest.fn(),
+ get: jest.fn(),
+ delete: jest.fn(),
+};
+
+export default mockFeatureStore;
diff --git a/packages/sdk/fastly/src/utils/validateOptions.ts b/packages/sdk/fastly/src/utils/validateOptions.ts
new file mode 100644
index 0000000000..c65fa083aa
--- /dev/null
+++ b/packages/sdk/fastly/src/utils/validateOptions.ts
@@ -0,0 +1,37 @@
+import { LDOptions as LDOptionsCommon } from '@launchdarkly/js-server-sdk-common';
+
+/**
+ * The Launchdarkly Edge SDKs configuration options. Only logger is officially
+ * supported. sendEvents is unsupported and is only included as a beta
+ * preview.
+ */
+export type LDOptions = Pick;
+
+/**
+ * The internal options include featureStore because that's how the LDClient
+ * implementation expects it.
+ */
+export type LDOptionsInternal = LDOptions & Pick;
+
+const validateOptions = (sdkKey: string, options: LDOptionsInternal) => {
+ const { featureStore, logger, sendEvents, ...rest } = options;
+ if (!sdkKey) {
+ throw new Error('You must configure the client with a client key');
+ }
+
+ if (!featureStore || typeof featureStore !== 'object' || !featureStore.get) {
+ throw new Error('You must configure the client with a feature store');
+ }
+
+ if (!logger) {
+ throw new Error('You must configure the client with a logger');
+ }
+
+ if (JSON.stringify(rest) !== '{}') {
+ throw new Error(`Invalid configuration: ${Object.keys(rest).toString()} not supported`);
+ }
+
+ return true;
+};
+
+export default validateOptions;
diff --git a/packages/sdk/fastly/tsconfig.eslint.json b/packages/sdk/fastly/tsconfig.eslint.json
new file mode 100644
index 0000000000..56c9b38305
--- /dev/null
+++ b/packages/sdk/fastly/tsconfig.eslint.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/sdk/fastly/tsconfig.json b/packages/sdk/fastly/tsconfig.json
new file mode 100644
index 0000000000..7ff06dea45
--- /dev/null
+++ b/packages/sdk/fastly/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ // Uses "." so it can load package.json.
+ "rootDir": ".",
+ "outDir": "dist",
+ "target": "es2017",
+ "lib": ["es6"],
+ "module": "commonjs",
+ "strict": true,
+ "noImplicitOverride": true,
+ "allowSyntheticDefaultImports": true,
+ "sourceMap": true,
+ "declaration": true,
+ "declarationMap": true, // enables importers to jump to source
+ "resolveJsonModule": true,
+ "stripInternal": true,
+ "moduleResolution": "node",
+ "types": ["jest", "node"],
+ "skipLibCheck": true
+ },
+ "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__", "example"]
+}
diff --git a/packages/sdk/fastly/tsconfig.ref.json b/packages/sdk/fastly/tsconfig.ref.json
new file mode 100644
index 0000000000..832c1d8dd7
--- /dev/null
+++ b/packages/sdk/fastly/tsconfig.ref.json
@@ -0,0 +1,7 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src/**/*", "package.json", "src/**/testData.json"],
+ "compilerOptions": {
+ "composite": true
+ }
+}
diff --git a/packages/sdk/fastly/tsup.config.ts b/packages/sdk/fastly/tsup.config.ts
new file mode 100644
index 0000000000..7ec3b3486b
--- /dev/null
+++ b/packages/sdk/fastly/tsup.config.ts
@@ -0,0 +1,26 @@
+// It is a dev dependency and the linter doesn't understand.
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { defineConfig } from 'tsup';
+
+export default defineConfig({
+ entry: {
+ index: 'src/index.ts',
+ },
+ minify: true,
+ format: ['esm', 'cjs'],
+ splitting: false,
+ sourcemap: false,
+ clean: true,
+ noExternal: ['@launchdarkly/js-server-sdk-common'],
+ dts: true,
+ metafile: true,
+ esbuildOptions(opts) {
+ // This would normally be `^_(?!meta|_)`, but go doesn't support negative look-ahead assertions,
+ // so we need to craft something that works without it.
+ // So start of line followed by a character that isn't followed by m or underscore, but we
+ // want other things that do start with m, so we need to progressively handle more characters
+ // of meta with exclusions.
+ // eslint-disable-next-line no-param-reassign
+ opts.mangleProps = /^_([^m|_]|m[^e]|me[^t]|met[^a])/;
+ },
+});
diff --git a/packages/sdk/fastly/typedoc.json b/packages/sdk/fastly/typedoc.json
new file mode 100644
index 0000000000..7ac616b544
--- /dev/null
+++ b/packages/sdk/fastly/typedoc.json
@@ -0,0 +1,5 @@
+{
+ "extends": ["../../../typedoc.base.json"],
+ "entryPoints": ["src/index.ts"],
+ "out": "docs"
+}
diff --git a/release-please-config.json b/release-please-config.json
index b25cbcf8eb..6d542753ba 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -20,6 +20,15 @@
"src/createPlatformInfo.ts"
]
},
+ "packages/sdk/fastly": {
+ "extra-files": [
+ {
+ "type": "json",
+ "path": "example/package.json",
+ "jsonpath": "$.dependencies['@launchdarkly/fastly-server-sdk']"
+ }
+ ]
+ },
"packages/sdk/react-native": {},
"packages/sdk/server-node": {},
"packages/sdk/vercel": {
diff --git a/tsconfig.json b/tsconfig.json
index 2059110636..b5cb48d036 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -69,6 +69,9 @@
},
{
"path": "./packages/telemetry/browser-telemetry/tsconfig.ref.json"
+ },
+ {
+ "path": "./packages/sdk/fastly/tsconfig.ref.json"
}
]
-}
+}
\ No newline at end of file