Skip to content
Open
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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ jobs:
- name: Run ESLint
run: npm run lint

test:
name: Unit & Integration tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests with --detectOpenHandles
run: npm test -- --detectOpenHandles

audit:
name: Dependency vulnerability gate
runs-on: ubuntu-latest
Expand Down
230 changes: 221 additions & 9 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ paths:
$ref: '#/components/schemas/IndexedEvent'
pagination:
$ref: '#/components/schemas/Pagination'
/api/song/{id}/stats:
get:
summary: Get song statistics
description: Returns aggregated playback and engagement metrics for a song. Public callers receive total plays and saves; the song artist or admins receive full metrics including unique listeners and revenue.
/api/song/{id}:
parameters:
- name: id
Expand Down Expand Up @@ -230,6 +234,105 @@ paths:
- name: id
in: path
required: true
description: Unique song identifier (UUID).
schema:
type: string
format: uuid
- name: window
in: query
required: false
description: Aggregation time window. Default is 7d.
schema:
type: string
enum: [24h, 7d, 30d, 90d, all-time]
default: 7d
responses:
'200':
description: Song statistics retrieved successfully.
headers:
Cache-Control:
schema:
type: string
description: Caching policy (public or private with 300s TTL).
Vary:
schema:
type: string
example: Authorization
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
example: true
data:
$ref: '#/components/schemas/SongStats'
'400':
description: Invalid window parameter.
'404':
description: Song not found.

/api/song/artist/{artistId}/stats:
get:
summary: Get artist aggregated statistics via song route
description: Returns catalog-wide metrics for the specified artist. Public callers receive plays and saves; the artist owner or admins receive full metrics including revenue and unique listeners.
parameters:
- name: artistId
in: path
required: true
description: Unique artist identifier (UUID).
schema:
type: string
format: uuid
- name: window
in: query
required: false
description: Aggregation time window. Default is 7d.
schema:
type: string
enum: [24h, 7d, 30d, 90d, all-time]
default: 7d
responses:
'200':
description: Artist statistics retrieved successfully.
'400':
description: Invalid window parameter.
'404':
description: Artist not found.

/api/artist/{id}/stats:
get:
summary: Get artist aggregated statistics
description: Returns catalog-wide metrics for the specified artist. Public callers receive plays and saves; the artist owner or admins receive full metrics including revenue and unique listeners.
parameters:
- name: id
in: path
required: true
description: Unique artist identifier (UUID).
schema:
type: string
format: uuid
- name: window
in: query
required: false
description: Aggregation time window. Default is 7d.
schema:
type: string
enum: [24h, 7d, 30d, 90d, all-time]
default: 7d
responses:
'200':
description: Artist statistics retrieved successfully.
'400':
description: Invalid window parameter.
'404':
description: Artist not found.

/api/webhooks/{id}/deliveries:
get:
summary: List webhook delivery attempts
description: Returns paginated delivery history for a webhook subscription (newest first). Only accessible by the subscription owner.
schema:
type: string
format: uuid
Expand Down Expand Up @@ -282,35 +385,78 @@ paths:
- name: id
in: path
required: true
description: Unique webhook subscription identifier (UUID).
schema:
type: string
format: uuid
- name: page
in: query
required: false
schema:
type: integer
minimum: 1
default: 1
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: A page of versions with pagination metadata.
description: Delivery attempts list.
content:
application/json:
schema:
type: object
properties:
success:
type: boolean
example: true
data:
type: array
items:
$ref: '#/components/schemas/WebhookDelivery'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
description: Unauthorized.
'403':
description: The caller does not own the song.
/api/song/{id}/versions/{version}:
get:
summary: Get one song version
description: Owner or admin only.
description: Forbidden - caller is not the subscription owner.
'404':
description: Webhook subscription not found.

/api/webhooks/{id}/deliveries/{deliveryId}/redeliver:
post:
summary: Redeliver a webhook attempt
description: Resends the event payload of a previous delivery attempt. Only accessible by the subscription owner.
parameters:
- name: id
in: path
required: true
description: Webhook subscription UUID.
schema:
type: string
format: uuid
- name: deliveryId
in: path
required: true
description: Webhook delivery attempt UUID.
schema:
type: string
format: uuid
responses:
'200':
description: Webhook redelivered successfully.
'400':
description: No payload stored for this delivery.
'401':
description: Unauthorized.
'403':
description: Forbidden - caller is not the subscription owner.
'404':
description: Subscription or delivery record not found.
'502':
description: Destination endpoint failed after retry attempts.
schema:
type: string
format: uuid
Expand Down Expand Up @@ -468,6 +614,71 @@ components:
updatedAt:
type: string
format: date-time
SongStats:
type: object
properties:
window:
type: string
example: 7d
from:
type: string
format: date-time
to:
type: string
format: date-time
totals:
type: object
properties:
plays:
type: integer
example: 120
saves:
type: integer
example: 30
uniqueListeners:
type: integer
description: Owner and admin only.
example: 45
revenueStroops:
type: string
description: Owner and admin only.
example: "5000000"
WebhookDelivery:
type: object
properties:
id:
type: string
format: uuid
subscriptionId:
type: string
format: uuid
eventId:
type: string
eventType:
type: string
attempt:
type: integer
example: 1
status:
type: string
enum: [pending, succeeded, failed]
responseStatus:
type: integer
nullable: true
example: 200
durationMs:
type: integer
nullable: true
example: 45
error:
type: string
nullable: true
payload:
type: object
nullable: true
createdAt:
type: string
format: date-time
Pagination:
type: object
properties:
Expand All @@ -483,3 +694,4 @@ components:
totalPages:
type: integer
example: 3

2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ module.exports = {
// Strip AI vendor keys and pin AI_PROVIDER=noop before every test file so
// no test can depend on a live AI provider (#287).
setupFiles: ["<rootDir>/tests/setup/aiProviderIsolation.js"],
// Global teardown to clean up in-process timers and mock connections (Issue #834)
setupFilesAfterEnv: ["<rootDir>/tests/setup/jestTeardown.ts"],
moduleNameMapper: {
"^reflect-metadata$": "<rootDir>/node_modules/reflect-metadata",
// wasm-attestation-bindings is ESM-only and uses `import.meta`, which
Expand Down
Loading