Skip to content
Closed
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
16 changes: 8 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Public-safe example: fill in your OWN values in an ignored .env file.
# DO NOT commit real API keys, write keys, Cloudinary secrets or DigitalOcean tokens.
# Every NUXT_PUBLIC_* variable is exposed to browsers and static builds.
#SERVER_PRESET=digital-oceans
WEBSITE_NAME="ePlus OnAir — Listen Beyond Borders."
DESCRIPTION="ePlus OnAir — Listen Beyond Borders. Discover and listen to live radio stations worldwide."
Expand All @@ -17,21 +20,18 @@ CORS_SERVER_4="https://e-cors.netlify.app/"
NUXT_PUBLIC_API_FRESHPING="https://api.freshping.io/v1/"
# Algolia
ALGOLIA_APPLICATION_ID="N87NFQVY0Z"
ALGOLIA_API_KEY="73e93ee2ef3aa6919b7c6495767c7f9d"
ALGOLIA_WRITE_API_KEY="73e93ee2ef3aa6919b7c6495767c7f9d"
# Search-only Algolia key. Create your own restricted search key; never use an Admin/Write key.
ALGOLIA_API_KEY=""
# ALGOLIA_APPLICATION_ID="P2IANQRBXJ"
# ALGOLIA_API_KEY="59f763506ceef787c76d64a6d407cc49"
# ALGOLIA_WRITE_API_KEY="0b650d5ddd09a3b296db220d2544ae18"
# Zing Server
NUXT_PUBLIC_API_ZING_LIVE_STREAM_PROGRAM_DETAIL="https://zing.eplus.dev/api/live-stream-program-detail?id="
NUXT_PUBLIC_API_ZING_GET_LYRIC="https://zing.eplus.dev/api/lyric?id="
# Cloudinary
CLOUDINARY_NAME="eplus-dev"
CLOUDINARY_API_KEY="854137316289745"
CLOUDINARY_API_SECRET="DfxLkt8brnb-AqyMiALNtW0Hnqw"
# Cloudinary upload/API credentials must never be stored here or exposed via NUXT_PUBLIC_*.
# For server-side workflows only, configure CLOUDINARY_API_KEY and
# CLOUDINARY_API_SECRET as GitHub environment secrets or local .env values.
NUXT_PUBLIC_CLOUDINARY_BASE_URL="http://localhost:3000"
# DigitalOcean
NUXT_PUBLIC_DIGITALOCEAN_ENDPOINT="https://api.digitalocean.com/v2/apps/7cebe9c4-324f-4d47-a41f-ebfe3baedaf6/deployments?page=1&per_page=1"
NUXT_PUBLIC_DIGITALOCEAN_API_KEY="dop_v1_d8ceeb4ae52c6632602dab89d251eacecfe8a6e75c406e2c69ca3c42ed46d8e5"
# Sitemap
NUXT_PUBLIC_SITE_URL="http://localhost:3000"
19 changes: 18 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,21 @@ updates:
schedule:
interval: "weekly"
labels:
- "dependencies"
- "dependencies"

# Keep runtime/frontend dependencies visible to maintainers without
# opening dozens of near-identical public dependency PRs.
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 3
groups:
minor-and-patch:
patterns:
- "*"
update-types:
- "minor"
- "patch"
labels:
- "dependencies"
4 changes: 2 additions & 2 deletions .github/workflows/deploy-github-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
exit 1
fi
if [ -z "$PAGES_DEPLOY_TOKEN" ]; then
echo "::error::Missing PAGES_DEPLOY_TOKEN in the private source repository."
echo "::error::Missing PAGES_DEPLOY_TOKEN in the source repository's environment secrets."
exit 1
fi
auth="$(printf 'x-access-token:%s' "$PAGES_DEPLOY_TOKEN" | base64 -w0)"
Expand Down Expand Up @@ -143,7 +143,7 @@ jobs:
fi
echo "Validated target $TARGET_REPOSITORY and read/write PAT permissions."

- name: Check out private source
- name: Check out trusted source
if: steps.current.outputs.run == 'true'
uses: actions/checkout@v7
with:
Expand Down
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Contributing to ePlus OnAir

Thanks for contributing improvements to the open development of the ePlus OnAir
website. The source is public; please read the [security policy](./SECURITY.md)
and [coding guide](./AGENTS.md) before proposing changes.

## Before opening a PR

1. For normal fixes/features, branch from `develop` and open a PR against
`develop`; `main` is production. For production hotfixes, coordinate
with maintainers so they are also applied to `develop`.
2. Run `npm ci` using the Node version in `.node-version`, then `npm test`.
Run `npm run test:ui` where browser behavior is changed; Playwright
is optional in the default CI. For substantial rendering/SEO changes,
run `npm run generate:site`.
3. Describe the issue, user impact, changes, test results and (if UI-related)
screenshots at desktop and mobile sizes. Avoid unrelated generated files.
4. Keep UI text translated into all supported locales; check keyboard
navigation, touch targets and light/dark contrast.
5. Never include credentials, private .env files, tokens, personal user data,
third-party copyrighted source code, or unlicensed artwork in a PR.
The ignored `.env` file is only for local development; `.env.example`
must remain safe to publish.
6. If contributing a new station, verify its stream source, name, country,
artwork permission and official site; do not embed short-lived access
tokens or upload private/copyrighted files.
7. If a security issue is discovered, report it **privately** as described
in [SECURITY.md](./SECURITY.md), not in a public issue.

Source code publication does not itself grant permission to redistribute
radio station logos, audio streams, third-party media or proprietary metadata.
A repository source license will need to be selected and published separately.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**Website:** https://radio.eplus.dev · **Staging:** https://new-radio.eplus.dev

> [!IMPORTANT]
> This **private repository is the canonical source** for ePlus OnAir: Nuxt application, player, station catalog, tests, and release automation. The older `ePlus-DEV/get-radio` crawler is legacy history, not an active deployment source. Public Pages repositories hold generated artifacts, not application source.
> This **public repository is the canonical source** for ePlus OnAir: Nuxt application, player, station catalog, tests, and release automation. The older `ePlus-DEV/get-radio` crawler is legacy history, not an active deployment source. Public Pages repositories hold generated artifacts, not application source.

## Repository and environment map

Expand All @@ -17,7 +17,7 @@
- `develop` is the integration and staging branch. Open feature/bugfix PRs against it.
- `main` is the released source. Promote reviewed changes from `develop` through a release PR. Hotfixes start at `main` and must be brought back to `develop`.
- `master` is a legacy backup with separate history. Never reset, force-push, or deploy it as the current application.
- The destination repositories use their own `main` branches for the **static website output**; these are not the private source branch.
- The destination repositories use their own `main` branches for the **static website output**; these are not the application source branch.
- The custom hostnames and DNS must be verified separately. A successful GitHub push alone does not prove the live DNS is served by Pages.

**SEO isolation:** `develop` staging is always `noindex`; `main` production builds are indexable (`index, follow`) with canonical URLs and a sitemap. Production-only GA4 (`G-ETDY41KBBJ`) is enabled on the canonical production hostname. Verify DNS and HTTPS separately from GitHub Pages deployment.
Expand Down Expand Up @@ -80,7 +80,7 @@ npm run test:ui # Optional Playwright browser tests
5. The deployment updates `main` in the matching public Pages repository, re-creates `CNAME`, and **preserves its root `README.md`** during the destructive generated-site sync.
6. Confirm the Actions job succeeded and inspect the destination commit, deep-linked pages, images, and the configured custom hostname. Keep DNS/SEO cutover as an independent, reviewed task.

Required setup in the private source repository: GitHub Environments `development` / `production`, with `NUXT_PUBLIC_SITE_URL` set respectively to `https://new-radio.eplus.dev` / `https://radio.eplus.dev`; `PAGES_DEPLOY_TOKEN` must have scoped write access to both public Pages repositories. Do not expose this token to untrusted PR workflows.
Required setup in the source repository (store all credentials as GitHub **Secrets**, never in code): GitHub Environments `development` / `production`, with `NUXT_PUBLIC_SITE_URL` set respectively to `https://new-radio.eplus.dev` / `https://radio.eplus.dev`; `PAGES_DEPLOY_TOKEN` must have scoped write access to both public Pages repositories. Do not expose this token to untrusted PR workflows.

All four workflows share a repository-wide concurrency queue. The workflows are:

Expand Down Expand Up @@ -110,8 +110,16 @@ Smaller pushes do **not** lift GitHub Pages' published-site 1 GB limit; keep the

See [SEO architecture](./docs/SEO.md).

## Security when working in this public repository

- **Never commit real values** to `.env.example`, code, tests, documentation or issue/PR comments. See [security policy](./SECURITY.md).
- Keep only browser-safe keys under `NUXT_PUBLIC_*`. Never include provider admin/write keys, Cloudinary secrets, DigitalOcean tokens or GitHub publishing tokens.
- Place deployment PATs in GitHub **Environment Secrets**, restrict them to the exact Pages output repositories and Contents write access, and require approval for the `production` environment where practical.
- Use read-only CI for untrusted fork pull requests. Do not make `pull_request_target` workflows checkout untrusted PR code and run it with secrets.
- A credential in Git history must be **revoked/rotated** even if removed from the latest commit; deleting a line does not undo exposure.

## Contribution and maintainership

Follow [AGENTS.md](./AGENTS.md), [project context](./docs/PROJECT_CONTEXT.md), and [radio synchronization notes](./docs/instant-audio-sync.md). Keep UI text translated, validate station streams and artwork paths, test changes, and avoid committing secrets.
Follow [CONTRIBUTING.md](./CONTRIBUTING.md), [AGENTS.md](./AGENTS.md), [project context](./docs/PROJECT_CONTEXT.md), and [radio synchronization notes](./docs/instant-audio-sync.md). Keep UI text translated, validate station streams and artwork paths, test changes, and avoid committing secrets.

**License:** Maintained by ePlus.DEV; consult repository licensing information before redistribution.
**License:** No open-source license has been selected yet. Public visibility alone does not grant reuse or redistribution rights. Before accepting code contributions or advertising this as open source, choose and add a LICENSE (for example MIT or Apache-2.0 after checking third-party assets and dependencies).
46 changes: 46 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Security policy

## Reporting a vulnerability

Please **do not publish vulnerabilities, passwords, private keys, provider API tokens,
or full exploit details in a public GitHub issue or pull request**.

Report security concerns privately to **admin@eplus.dev** (subject:
`ePlus OnAir security report`), or use GitHub's **Report a vulnerability**
option on the repository's **Security** tab if private vulnerability reporting
is enabled. Include a concise description, affected file or endpoint,
impact, and reproducible steps without publishing live credentials.

This is a community-maintained site. A specific response or fix SLA is not guaranteed.

## Secrets and public deployment

- `main` and `develop` are **public source** branches. Public forks,
PR diffs and commit history can permanently expose any committed value.
- Never store live credentials in `.env.example`, `NUXT_PUBLIC_*`,
source code, tests, screenshots, CI logs, discussions or PR comments.
- `NUXT_PUBLIC_*` values are included in client bundles/static pages.
Only put information there that is safe for every visitor to see.
- The search-only Algolia key (if configured) must be limited to **search**,
restricted to the appropriate indexes. **Never** use an Admin/Write key.
- Publishing PATs belong in GitHub **Environment Secrets**, ideally with
access only to the specific destination repositories. Review and rotate
those tokens regularly, use minimal permissions, and protect releases.
- Do not let untrusted fork PR code access write tokens. Prefer read-only
PR checks and separate trusted deployments.

If a secret was previously committed, **revoke or rotate it immediately**.
Removing it from `main` is not enough: earlier commits, tags, forks,
clones, caches and pull-request diffs may still contain it. After revocation,
consider GitHub's documented sensitive-data history-removal process,
coordinate with collaborators and recheck the full public history.

## Vulnerability classes

Particularly useful reports include credential leakage, malicious
station artwork or catalog payloads, dependency vulnerabilities,
supply-chain / GitHub Actions permission issues, stored XSS, unsafe
cross-origin requests and data/privacy leaks.

Third-party radio streams, station logos and music programming belong to
their respective operators and may be subject to separate rights or terms.
2 changes: 1 addition & 1 deletion docs/CI_BUILD_COST.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ePlus OnAir CI build-cost policy

The private source uses **`develop` for staging** and **`main` for production**.
The public source repository uses **`develop` for staging** and **`main` for production**.
This document is about PR validation only, not publishing or DNS.

## Two CI paths
Expand Down
2 changes: 1 addition & 1 deletion docs/SEO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This file is the durable SEO checklist for ePlus OnAir. It records what is imple

## GitHub Pages and production indexing policy

GitHub Pages publishes generated static output from the private source into
GitHub Pages publishes generated static output from the public source repository into
two public repositories with **custom hostnames** and `NUXT_APP_BASE_URL=/`:

- `develop` → `ePlus-DEV/onair-staging-pages` → `https://new-radio.eplus.dev`:
Expand Down
2 changes: 0 additions & 2 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ export default defineNuxtConfig({
apiZingLiveStreamProgramDetail:
process.env.NUXT_PUBLIC_API_ZING_LIVE_STREAM_PROGRAM_DETAIL,
apiZingGetLyric: process.env.NUXT_PUBLIC_API_ZING_GET_LYRIC,
digitaloceanEndpoint: process.env.NUXT_PUBLIC_DIGITALOCEAN_ENDPOINT,
digitaloceanApiKey: process.env.NUXT_PUBLIC_DIGITALOCEAN_API_KEY,
x: process.env.NUXT_PUBLIC_X,
facebook: process.env.NUXT_PUBLIC_FACEBOOK,
github: process.env.NUXT_PUBLIC_GITHUB,
Expand Down
37 changes: 37 additions & 0 deletions tests/public-repository-security.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';

const read = path => readFile(new URL('../' + path, import.meta.url), 'utf8');

test('the public .env.example does not contain credential values', async () => {
const content = await read('.env.example');
assert.doesNotMatch(content, /dop_v1_[a-z0-9]+/i);
assert.doesNotMatch(content, /(?:ghp_|gho_|github_pat_)[a-zA-Z0-9_]+/);
assert.doesNotMatch(content, /(?:sk_live_|sk_test_)[a-zA-Z0-9]+/);
assert.doesNotMatch(content, /^\s*(?:CLOUDINARY_API_SECRET|CLOUDINARY_API_KEY|ALGOLIA_WRITE_API_KEY|NUXT_PUBLIC_DIGITALOCEAN_API_KEY)\s*=/m);
for (const line of content.split('\n')) {
if (line.startsWith('#')) continue;
const match = line.match(/^\s*([\w]+(?:API_KEY|API_SECRET|TOKEN|PASSWORD))\s*=\s*(.*)$/i);
if (match) assert.ok(/^(?:""|'')?$/.test(match[2].trim()),
`Credential placeholder for ${match[1]} must remain empty`);
}
});

test('Nuxt public runtime config must never export administrative provider credentials', async () => {
const config = await read('nuxt.config.ts');
assert.doesNotMatch(config, /digitaloceanApiKey\s*:/);
assert.doesNotMatch(config, /NUXT_PUBLIC_DIGITALOCEAN_API_KEY/);
assert.doesNotMatch(config, /CLOUDINARY_API_SECRET/);
const readme = await read('README.md');
assert.doesNotMatch(readme, /This \*\*private repository\*\*/);
assert.match(readme, /SECURITY\.md/);
});

test('public security and contribution policy are documented', async () => {
const [policy, guide] = await Promise.all([read('SECURITY.md'), read('CONTRIBUTING.md')]);
assert.match(policy, /revoke or rotate/i);
assert.match(policy, /NUXT_PUBLIC_\*/);
assert.match(guide, /develop/);
assert.match(guide, /npm test/);
});
Loading