Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM denoland/deno:2.7.13
FROM denoland/deno:2.8.0

# Install tools
RUN apt-get update && \
Expand Down
15 changes: 5 additions & 10 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
with:
fetch-depth: 0

- name: Setup Deno v2.7.14
- name: Setup Deno v2.8.0
uses: denoland/setup-deno@v2
with:
deno-version: v2.7.14
deno-version: v2.8.0

- name: Setup LCOV
run: sudo apt install -y lcov
Expand All @@ -35,7 +35,7 @@ jobs:
run: deno task cover

- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v8.0.0
uses: sonarsource/sonarqube-scan-action@v8.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
if: env.SONAR_TOKEN != ''
Expand All @@ -45,7 +45,7 @@ jobs:
strategy:
fail-fast: false
matrix:
deno-version: [v1.46.3, v2.7.14]
deno-version: [v2.8.0]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}

Expand All @@ -58,10 +58,5 @@ jobs:
with:
deno-version: ${{ matrix.deno-version }}

- name: Run tests (Deno v1)
if: "contains(matrix.deno-version, 'v1')"
run: deno task test-v1

- name: Run tests (Deno v2)
if: "contains(matrix.deno-version, 'v2')"
- name: Run tests
run: deno task test
8 changes: 4 additions & 4 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
steps:
- name: Get PR details
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v9
with:
script: |
const pr = await github.rest.pulls.get({
Expand All @@ -33,10 +33,10 @@ jobs:
ref: ${{ steps.pr.outputs.head_sha }}
fetch-depth: 0

- name: Setup Deno v2.7.14
- name: Setup Deno v2.8.0
uses: denoland/setup-deno@v2
with:
deno-version: v2.7.14
deno-version: v2.8.0

- name: Setup LCOV
run: sudo apt install -y lcov
Expand All @@ -51,7 +51,7 @@ jobs:
run: deno task cover

- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v8.0.0
uses: sonarsource/sonarqube-scan-action@v8.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
if: env.SONAR_TOKEN != ''
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ A Deno SDK for Switcher API

[![Master CI](https://github.com/switcherapi/switcher-client-deno/actions/workflows/master.yml/badge.svg)](https://github.com/switcherapi/switcher-client-deno/actions/workflows/master.yml)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=switcherapi_switcher-client-deno&metric=alert_status)](https://sonarcloud.io/dashboard?id=switcherapi_switcher-client-deno)
![Known Vulnerabilities](https://snyk.io/test/github/switcherapi/switcher-client-deno/badge.svg)
[![deno.land/x/switcher4deno](https://shield.deno.dev/x/switcher4deno)](https://deno.land/x/switcher4deno)
[![JSR](https://jsr.io/badges/@switcherapi/switcher-client-deno)](https://jsr.io/@switcherapi/switcher-client-deno)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Slack: Switcher-HQ](https://img.shields.io/badge/slack-@switcher/hq-blue.svg?logo=slack)](https://switcher-hq.slack.com/)

</div>

<div align="center">
<img src="https://github.com/switcherapi/switcherapi-assets/blob/master/logo/switcherapi_denoclient_transparency_1280.png" alt="Switcher API: Deno Client" />
</div>
***

![Switcher API: Deno Client: Cloud-based Feature Flag API](https://github.com/switcherapi/switcherapi-assets/blob/master/logo/switcherapi_deno_client.png)


## Table of Contents

Expand Down Expand Up @@ -64,7 +66,7 @@ A Deno SDK for Switcher API

### Prerequisites

- **Deno**: Version 1.4x, 2.x or above
- **Deno**: Version 2.x or above
- **Permissions**:

| Permission | Required For |
Expand All @@ -73,7 +75,6 @@ A Deno SDK for Switcher API
| `--allow-write` | Writing snapshot files (if enabled) |
| `--allow-net` | Communicating with the Switcher API |
| `--allow-env` | Accessing environment variables for configuration |
| `--unstable-http` | Required for custom SSL certificates in Deno v1.4x (see [Advanced Options](#advanced-options)) |

### Installation

Expand Down Expand Up @@ -136,7 +137,8 @@ Client.buildContext({
restrictRelay: true, // Relay restrictions in local mode
regexSafe: true, // Prevent reDOS attacks
certPath: './certs/ca.pem', // SSL certificate path
remoteTimeout: 2000 // Remote Criteria API timeout (milliseconds)
remoteTimeout: 2000, // Remote Criteria API timeout (milliseconds)
autoRefreshToken: true, // Automatically refresh auth token before expiration
});
```

Expand All @@ -157,6 +159,7 @@ Client.buildContext({
| `regexMaxTimeLimit` | number | Regex timeout in milliseconds |
| `certPath` | string | Path to SSL certificate file |
| `remoteTimeout` | number | Remote Criteria API timeout in milliseconds |
| `autoRefreshToken` | boolean | Automatically refresh auth token before expiration |

> **⚠️ Note on regexSafe**: This feature protects against reDOS attacks but uses Web Workers, which are incompatible with compiled executables.

Expand Down
3 changes: 1 addition & 2 deletions deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
{
"name": "@switcherapi/switcher-client-deno",
"version": "2.4.1",
"version": "2.5.0",
"description": "Switcher Client Deno is a Feature Flag Deno Client SDK for Switcher API",
"tasks": {
"cache-reload": "deno cache --reload --lock=deno.lock mod.ts",
"fmt": "deno fmt mod.ts src/ --options-single-quote --options-line-width=120 --check",
"fmt:fix": "deno fmt mod.ts src/ --options-single-quote --options-line-width=120",
"test": "deno test --allow-read --allow-net --allow-write --allow-env --allow-import --coverage=coverage",
"test-v1": "deno test --allow-read --allow-net --allow-write --allow-env --unstable-http --coverage=coverage",
"clean": "rm -rf ./npm ./coverage ./generated-snapshots",
"lcov": "deno coverage coverage --lcov --output=coverage/report.lcov",
"cover": "deno task clean && deno task test && deno task lcov && genhtml -o coverage/html coverage/report.lcov",
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sonar.projectKey=switcherapi_switcher-client-deno
sonar.projectName=switcher-client-deno
sonar.organization=switcherapi
sonar.projectVersion=2.4.1
sonar.projectVersion=2.5.0

sonar.javascript.lcov.reportPaths=coverage/report.lcov

Expand Down
5 changes: 5 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as remote from './lib/remote.ts';
import * as util from './lib/utils/index.ts';
import Bypasser from './lib/bypasser/index.ts';
import {
DEFAULT_AUTO_REFRESH_TOKEN,
DEFAULT_ENVIRONMENT,
DEFAULT_FREEZE,
DEFAULT_LOCAL,
Expand Down Expand Up @@ -66,6 +67,7 @@ export class Client {
local: util.get(options?.local, DEFAULT_LOCAL),
freeze: util.get(options?.freeze, DEFAULT_FREEZE),
logger: util.get(options?.logger, DEFAULT_LOGGER),
autoRefreshToken: util.get(options?.autoRefreshToken, DEFAULT_AUTO_REFRESH_TOKEN),
});

// Initialize Auth
Expand Down Expand Up @@ -98,6 +100,9 @@ export class Client {
GlobalOptions.updateOptions({ snapshotWatcher: options.snapshotWatcher });
this.watchSnapshot();
},
[SWITCHER_OPTIONS.AUTO_REFRESH_TOKEN]: () => {
GlobalOptions.updateOptions({ autoRefreshToken: options.autoRefreshToken });
},
};

for (const key in optionsHandler) {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const DEFAULT_LOCAL = false;
export const DEFAULT_FREEZE = false;
export const DEFAULT_LOGGER = false;
export const DEFAULT_TEST_MODE = false;
export const DEFAULT_AUTO_REFRESH_TOKEN = false;
export const DEFAULT_REGEX_MAX_BLACKLISTED = 50;
export const DEFAULT_REGEX_MAX_TIME_LIMIT = 3000;

Expand All @@ -17,4 +18,5 @@ export enum SWITCHER_OPTIONS {
REGEX_MAX_TIME_LIMIT = 'regexMaxTimeLimit',
CERT_PATH = 'certPath',
REMOTE_TIMEOUT = 'remoteTimeout',
AUTO_REFRESH_TOKEN = 'autoRefreshToken',
}
12 changes: 6 additions & 6 deletions src/lib/globals/globalAuth.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
export class GlobalAuth {
private static _token?: string;
private static _exp?: number;
private static _url?: string;
private static _token: string;
private static _exp: number;

static init(url: string | undefined) {
this._url = url;
this._token = undefined;
this._exp = undefined;
this._token = '';
this._exp = 0;
}

static get token() {
return this._token;
}

static set token(value: string | undefined) {
static set token(value: string) {
this._token = value;
}

static get exp() {
return this._exp;
}

static set exp(value: number | undefined) {
static set exp(value: number) {
this._exp = value;
}

Expand Down
4 changes: 4 additions & 0 deletions src/lib/globals/globalOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ export class GlobalOptions {
static get restrictRelay() {
return this.options.restrictRelay;
}

static get autoRefreshToken() {
return this.options.autoRefreshToken;
}
}
36 changes: 33 additions & 3 deletions src/lib/remoteAuth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RetryOptions, SwitcherContext } from '../types/index.d.ts';
import { GlobalAuth } from './globals/globalAuth.ts';
import { GlobalOptions } from './globals/globalOptions.ts';
import { auth, checkAPIHealth } from './remote.ts';
import DateMoment from './utils/datemoment.ts';
import * as util from './utils/index.ts';
Expand All @@ -10,23 +11,52 @@ import * as util from './utils/index.ts';
export class Auth {
private static context: SwitcherContext;
private static retryOptions: RetryOptions;
private static refreshTimer: NodeJS.Timeout | undefined;

static init(context: SwitcherContext) {
this.context = context;
GlobalAuth.init(context.url);
}

private static scheduleNextAuth() {
const msUntilExpiry = (GlobalAuth.exp * 1000) - Date.now();
const refreshAt = Math.max(msUntilExpiry - 5000, 0); // 5s before expiry

this.refreshTimer = setTimeout(() => {
this.authUpdate().then(() => {
this.scheduleNextAuth();
}).catch(() => {
this.terminateAutoRefresh();
});
}, refreshAt);
}

private static async authUpdate() {
const response = await auth(this.context);
GlobalAuth.token = response.token;
GlobalAuth.exp = response.exp;
}

static setRetryOptions(silentMode: string) {
this.retryOptions = {
retryTime: Number.parseInt(silentMode.slice(0, -1)),
retryDurationIn: silentMode.slice(-1),
};
}

static terminateAutoRefresh() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = undefined;
}
}

static async auth() {
const response = await auth(this.context);
GlobalAuth.token = response.token;
GlobalAuth.exp = response.exp;
await this.authUpdate();

if (GlobalOptions.autoRefreshToken && !this.refreshTimer) {
this.scheduleNextAuth();
}
}

static checkHealth() {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/snapshotWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as util from './utils/index.ts';
*/
export class SnapshotWatcher {
private _watcher: Deno.FsWatcher | undefined;
private readonly _watchDebounce = new Map<string, number>();
private readonly _watchDebounce = new Map<string, NodeJS.Timeout>();

async watchSnapshot(environment: string, callback: {
success?: () => void | Promise<void>;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/utils/snapshotAutoUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default class SnapshotAutoUpdater {
private static _intervalId: number | undefined;
private static _intervalId: NodeJS.Timeout | undefined;

static schedule(
interval: number,
Expand All @@ -23,5 +23,6 @@ export default class SnapshotAutoUpdater {

static terminate() {
clearInterval(this._intervalId);
this._intervalId = undefined;
}
}
7 changes: 7 additions & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ export type SwitcherOptions = {
* If not set, it will use the default value (2000 ms)
*/
remoteTimeout?: number;

/**
* When enabled it will automatically refresh the token before it expires
*
* If not set, it will not refresh the token automatically
*/
autoRefreshToken?: boolean;
};

export type RetryOptions = {
Expand Down
7 changes: 2 additions & 5 deletions tests/switcher-integrated.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describe, it, assertExists, assertGreater, load } from './deps.ts';
import { assertTrue } from "./helper/utils.ts";

import { Client } from '../mod.ts';

Expand Down Expand Up @@ -47,7 +46,7 @@ describe('Switcher integrated test', () => {
assertGreater(Client.snapshotVersion, 0);
});

it('should check Switcher availability', async function () {
it('should check Switcher availability', function () {
if (!Deno.env.get('SWITCHER_API_KEY')) {
return;
}
Expand All @@ -56,9 +55,7 @@ describe('Switcher integrated test', () => {
Client.buildContext(contextSettings);

// test
await Client.checkSwitchers(['CLIENT_DENO_FEATURE']);

assertTrue(true);
return Client.checkSwitchers(['CLIENT_DENO_FEATURE']);
});

});
Loading
Loading