Skip to content

feat: add @descope/csp - CSP policy builder package#1312

Open
omercnet wants to merge 5 commits intomainfrom
feature/csp-builder-package
Open

feat: add @descope/csp - CSP policy builder package#1312
omercnet wants to merge 5 commits intomainfrom
feature/csp-builder-package

Conversation

@omercnet
Copy link
Member

Summary

Adds @descope/csp - a TypeScript-first, zero-dependency Content Security Policy (CSP) builder for Descope integrations.

Motivation

Customers deploying Descope flows need to configure CSP policies with environment-specific URLs (staging, production, self-hosted). Console-app also has complex CSP middleware that could be simplified and made reusable.

This package provides a clean, type-safe way to:

  • Generate secure CSP policies for Descope integrations
  • Customize API/CDN URLs per environment
  • Merge custom rules with Descope defaults
  • Use presets for common third-party integrations

Key Features

Core

  • ✅ Minimal Descope defaults (based on web-component CSP requirements)
  • ✅ Environment-specific URL customization (5 URLs: api, cdn, static, images, content)
  • ✅ Cryptographically secure nonce generation
  • ✅ Additive policy merging (never overwrites)
  • ✅ Framework-agnostic output (object + string)
  • ✅ Full TypeScript definitions
  • ✅ Zero runtime dependencies
  • ✅ ~2KB minified

Presets (from console-app)

  • googleFonts - Google Fonts integration
  • segment - Segment analytics
  • featureOS - Feature OS widgets
  • devRev - DevRev platform
  • jsdelivr - jsDelivr CDN fallback
  • npmRegistry - NPM registry access
  • descopeInternal - Descope internal tools

Usage Examples

Minimal

import { createDescopeCSP } from '@descope/csp';
const csp = createDescopeCSP();
// Output: "script-src 'self' https://static.descope.com https://descopecdn.com; ..."

With Environment URLs

const csp = createDescopeCSP({
  urls: {
    api: process.env.DESCOPE_API_URL || 'api.descope.com',
    cdn: process.env.DESCOPE_CDN_URL || 'descopecdn.com',
  }
});

With Nonce

import { generateNonce } from '@descope/csp';
const nonce = generateNonce();
const csp = createDescopeCSP({ nonce });

Console-App Migration

import { presets } from '@descope/csp';

const csp = createDescopeCSP({
  nonce,
  presets: [
    presets.googleFonts,
    presets.segment,
    presets.featureOS,
    presets.devRev,
    presets.jsdelivr,
    presets.npmRegistry,
    presets.descopeInternal,
  ],
});

Alignment with Web Component

Based on packages/sdks/web-component/src/app/index.html CSP requirements.

Web Component (minimal):

script-src: 'self' static.descope.com descopecdn.com
img-src: static.descope.com content.app.descope.com imgs.descope.com data:
connect-src: 'self' static.descope.com api.descope.com

Our Output (minimal):

script-src: 'self' https://static.descope.com https://descopecdn.com
img-src: https://static.descope.com https://content.app.descope.com https://imgs.descope.com data:
connect-src: 'self' https://static.descope.com https://api.descope.com

✅ Perfect match!

Testing

  • 50 tests - All passing ✅
  • 83% coverage - Statements
  • 97% coverage - Branches
  • 87% coverage - Lines

Tests cover:

  • Nonce generation (randomness, encoding)
  • Descope defaults (URLs, directives)
  • Policy merging (deduplication, additive)
  • CSP serialization (string formatting)
  • Builder API (all options)
  • All 7 presets

Package Structure

packages/libs/csp/
├── src/
│   ├── types.ts          # TypeScript definitions
│   ├── nonce.ts          # Cryptographic nonce generation
│   ├── defaults.ts       # Descope minimal defaults
│   ├── merge.ts          # Policy merging logic
│   ├── serialize.ts      # CSP string formatting
│   ├── builder.ts        # Main createDescopeCSP function
│   ├── presets.ts        # 7 presets (console-app integrations)
│   └── index.ts          # Public API
├── test/                 # Jest test suite (50 tests)
├── examples/             # Usage examples
├── jest.config.cjs       # Jest configuration
├── package.json
├── tsconfig.json
├── rollup.config.mjs
└── README.md             # Complete documentation

Use Cases

  1. Web Component Users - Customers deploying Descope flows can easily generate secure CSP with environment-specific URLs
  2. Console-App Migration - Console-app can replace its middleware.js CSP logic with this package
  3. SDK Integrations - Other Descope SDKs (nextjs-sdk, react-sdk) can use this for CSP generation

Next Steps

  • Publish to npm (after merge)
  • Migrate console-app to use this package
  • Add to main descope-js README
  • Consider integration into nextjs-sdk/react-sdk

Breaking Changes

None - this is a new package.

Checklist

  • Tests added and passing (50 tests, 83%+ coverage)
  • Documentation complete (README, examples, JSDoc)
  • TypeScript types exported
  • Zero runtime dependencies
  • Build succeeds (ESM + CJS)
  • Package.json configured correctly

- Zero-dependency CSP builder for Descope integrations
- Environment-specific URL customization (api, cdn, static, images)
- Cryptographically secure nonce generation
- Additive policy merging with Descope defaults
- TypeScript-first with full type definitions
- Framework-agnostic core (ESM + CJS)
- Built-in presets (Google Fonts, Segment)
- Based on web-component CSP requirements
- Package size: ~2KB minified
- Includes comprehensive README and examples
- Add content.app.descope.com to img-src (was missing)
- Remove unnecessary directives (default-src, media-src, object-src, etc.)
- Only include minimal required directives: script-src, img-src, connect-src
- Add style-src only when nonce is provided
- Match exact web-component CSP structure from src/app/index.html
Add all third-party integrations from console-app as reusable presets:
- featureOS: Feature OS widgets (script-src, frame-src)
- devRev: DevRev platform (script-src, connect-src, frame-src)
- jsdelivr: jsDelivr CDN fallback (script-src, connect-src)
- npmRegistry: NPM registry access for flow-components
- descopeInternal: Descope internal tools (dev-panel, static assets)

This allows console-app to easily migrate to this package while
keeping all existing CSP integrations as composable presets.

Updated README with preset documentation and console-app migration example.
- Add Jest configuration with 80%+ coverage thresholds
- Test nonce generation (6 tests)
- Test Descope defaults with URL customization (9 tests)
- Test CSP merging and deduplication (8 tests)
- Test CSP serialization (9 tests)
- Test builder with all options (12 tests)
- Test all 7 presets (8 tests)

Total: 52 tests, all passing
Coverage: 83% statements, 97% branches, 87% lines
Copilot AI review requested due to automatic review settings January 18, 2026 13:03
@vercel
Copy link

vercel bot commented Jan 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
access-key-management-widget Ready Ready Preview, Comment Jan 18, 2026 1:04pm
audit-management-widget Ready Ready Preview, Comment Jan 18, 2026 1:04pm
role-management-widget Ready Ready Preview, Comment Jan 18, 2026 1:04pm
user-management-widget Ready Ready Preview, Comment Jan 18, 2026 1:04pm
user-profile-widget Ready Ready Preview, Comment Jan 18, 2026 1:04pm

Request Review

@nx-cloud
Copy link

nx-cloud bot commented Jan 18, 2026

View your CI Pipeline Execution ↗ for commit cd587dd

Command Status Duration Result
nx affected --target lint --fix=true ❌ Failed 1m 17s View ↗
nx affected --target build ✅ Succeeded 3m 31s View ↗
nx affected --target licenseCheck ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-18 13:12:54 UTC

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds @descope/csp, a new TypeScript package for building Content Security Policy headers for Descope integrations. The package provides type-safe CSP generation with environment-specific URL customization, cryptographic nonce generation, additive policy merging, and presets for common third-party integrations (Google Fonts, Segment, etc.).

Changes:

  • New @descope/csp package with builder API, nonce generation, and 7 integration presets
  • Comprehensive test suite (50 tests) with 80%+ coverage thresholds
  • Full TypeScript definitions and dual ESM/CJS builds

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/libs/csp/package.json Package configuration with dual ESM/CJS exports, MIT license, zero runtime dependencies (tslib only)
packages/libs/csp/src/types.ts Complete TypeScript type definitions for CSP directives, options, and results
packages/libs/csp/src/nonce.ts Cryptographic nonce generation with browser/Node.js crypto fallback
packages/libs/csp/src/defaults.ts Descope default CSP rules with URL customization and HTTPS enforcement
packages/libs/csp/src/merge.ts Additive CSP policy merging with deduplication
packages/libs/csp/src/serialize.ts CSP-to-string serialization for HTTP headers
packages/libs/csp/src/builder.ts Main API combining defaults, presets, and custom extensions
packages/libs/csp/src/presets.ts 7 presets for Google Fonts, Segment, Feature OS, DevRev, jsDelivr, NPM, Descope internal
packages/libs/csp/src/index.ts Public exports (functions and types)
packages/libs/csp/test/*.test.ts Comprehensive test suite covering all modules (6 test files, 50 tests)
packages/libs/csp/rollup.config.mjs Rollup build configuration for ESM/CJS dual output with TypeScript declarations
packages/libs/csp/tsconfig.json TypeScript configuration targeting ES2017 with strict mode
packages/libs/csp/jest.config.cjs Jest configuration with coverage thresholds (80%+ for most metrics)
packages/libs/csp/README.md Complete documentation with API reference, examples, and framework integrations
packages/libs/csp/examples/*.ts Usage examples for Next.js middleware and basic usage patterns
pnpm-lock.yaml Updated lockfile with new package dependencies and web-components-ui version bumps
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

);
}

return btoa(Array.from(array, (byte) => String.fromCharCode(byte)).join(''));
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of btoa on line 26 will cause runtime errors in Node.js environments before version 18, as btoa is only available in Node.js 18+. Since the package targets ES2017 and includes Node.js environments (based on the dual crypto fallback), this could break for users on Node.js < 18.

Consider using Node.js's Buffer.from(data).toString('base64') when in a Node.js environment, or add a polyfill. The fallback pattern is already present for crypto, a similar approach should be used for base64 encoding.

Suggested change
return btoa(Array.from(array, (byte) => String.fromCharCode(byte)).join(''));
// Prefer browser/global btoa when available, otherwise fall back to Node's Buffer.
if (typeof btoa === 'function') {
return btoa(
Array.from(array, (byte) => String.fromCharCode(byte)).join(''),
);
}
if (typeof Buffer !== 'undefined') {
return Buffer.from(array).toString('base64');
}
throw new Error('No base64 implementation available');

Copilot uses AI. Check for mistakes.
coverageThreshold: {
global: {
branches: 80,
functions: 65,
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jest.config.cjs sets the function coverage threshold to 65%, but the PR description claims 87% coverage for lines and implies comprehensive coverage. This discrepancy suggests either the claimed coverage numbers are inaccurate or the threshold is set too low. Consider raising the function coverage threshold to match the actual achieved coverage, or verify the actual coverage metrics align with what's claimed in the PR description.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +15
} else if (typeof require !== 'undefined') {
const nodeCrypto = require('crypto');
nodeCrypto.randomFillSync(array);
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of dynamic require('crypto') may cause issues with bundlers and ESM-only environments. Since the package is configured as "type": "module" in package.json, and uses ESM imports throughout, this dynamic require could fail in pure ESM environments or cause bundler warnings.

Consider using a static import with conditional usage, or handle the Node.js crypto import more gracefully for ESM compatibility.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants