diff --git a/.changeset/fix-npm-publish-ci.md b/.changeset/fix-npm-publish-ci.md new file mode 100644 index 0000000..2d96979 --- /dev/null +++ b/.changeset/fix-npm-publish-ci.md @@ -0,0 +1,4 @@ +--- +--- + +Fix npm publish OIDC auth conflict and correct version to 3.0.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4659fea..665f614 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,12 +35,10 @@ jobs: git config user.name "scope3-wizard[bot]" git config user.email "${{ vars.SCOPE3_WIZARD_APP_ID }}+scope3-wizard[bot]@users.noreply.github.com" - # setup-node used directly: internal action doesn't support registry-url - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: '22' - registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c5a766..18556d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # @scope3/agentic-client -## 4.0.0 +## 3.0.0 ### Major Changes diff --git a/package.json b/package.json index 776dd63..67d8b04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scope3", - "version": "4.0.0", + "version": "3.0.0", "description": "Scope3 SDK - REST and MCP client for the Agentic Platform", "engines": { "node": ">=18" diff --git a/src/__tests__/webhook-server.test.ts b/src/__tests__/webhook-server.test.ts index 3e9c90d..86ad783 100644 --- a/src/__tests__/webhook-server.test.ts +++ b/src/__tests__/webhook-server.test.ts @@ -48,9 +48,9 @@ function makeRequest(options: { }); } -/** Pick a random high port to avoid test collisions */ +/** Use port 0 so the OS assigns a free ephemeral port */ function randomPort(): number { - return 10000 + Math.floor(Math.random() * 50000); + return 0; } describe('WebhookServer', () => { @@ -184,17 +184,16 @@ describe('WebhookServer', () => { describe('lifecycle', () => { it('should start and begin listening', async () => { - port = randomPort(); - server = new WebhookServer({ port }); + server = new WebhookServer({ port: randomPort() }); const logSpy = jest.spyOn(console, 'log').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(`listening on port ${port}`)); logSpy.mockRestore(); }); it('should stop gracefully', async () => { - port = randomPort(); - server = new WebhookServer({ port }); + server = new WebhookServer({ port: randomPort() }); const logSpy = jest.spyOn(console, 'log').mockImplementation(); await server.start(); await server.stop(); @@ -208,11 +207,11 @@ describe('WebhookServer', () => { }); it('should reject start when port is already in use', async () => { - port = randomPort(); - const first = new WebhookServer({ port }); + const first = new WebhookServer({ port: randomPort() }); await first.start(); + const boundPort = parseInt(new URL(first.getUrl()).port); - const second = new WebhookServer({ port }); + const second = new WebhookServer({ port: boundPort }); await expect(second.start()).rejects.toThrow(); await first.stop(); @@ -223,11 +222,11 @@ describe('WebhookServer', () => { describe('GET /health', () => { it('should return status ok', async () => { - port = randomPort(); - server = new WebhookServer({ port }); + server = new WebhookServer({ port: randomPort() }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const res = await makeRequest({ port, method: 'GET', path: '/health' }); expect(res.status).toBe(200); @@ -235,10 +234,10 @@ describe('WebhookServer', () => { }); it('should respond to /health even when secret is configured', async () => { - port = randomPort(); - server = new WebhookServer({ port, secret: 'my-secret' }); + server = new WebhookServer({ port: randomPort(), secret: 'my-secret' }); jest.spyOn(console, 'log').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); // /health is behind the auth middleware when secret is set, // so it should require auth too (express middleware applies globally) @@ -261,11 +260,11 @@ describe('WebhookServer', () => { describe('event dispatch', () => { beforeEach(async () => { - port = randomPort(); - server = new WebhookServer({ port }); + server = new WebhookServer({ port: randomPort() }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); }); it('should invoke the specific handler for a matching event type', async () => { @@ -386,11 +385,11 @@ describe('WebhookServer', () => { describe('invalid events', () => { beforeEach(async () => { - port = randomPort(); - server = new WebhookServer({ port }); + server = new WebhookServer({ port: randomPort() }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); }); it('should return 400 when body is missing the type field', async () => { @@ -451,11 +450,11 @@ describe('WebhookServer', () => { describe('handler errors', () => { beforeEach(async () => { - port = randomPort(); - server = new WebhookServer({ port }); + server = new WebhookServer({ port: randomPort() }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); }); it('should return 500 when a handler throws synchronously', async () => { @@ -549,11 +548,11 @@ describe('WebhookServer', () => { describe('authentication', () => { it('should not require auth when no secret is configured', async () => { - port = randomPort(); - server = new WebhookServer({ port }); + server = new WebhookServer({ port: randomPort() }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const res = await makeRequest({ port, @@ -565,10 +564,10 @@ describe('WebhookServer', () => { }); it('should return 401 when secret is set and no Authorization header is sent', async () => { - port = randomPort(); - server = new WebhookServer({ port, secret: 'test-secret' }); + server = new WebhookServer({ port: randomPort(), secret: 'test-secret' }); jest.spyOn(console, 'log').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const res = await makeRequest({ port, @@ -581,10 +580,10 @@ describe('WebhookServer', () => { }); it('should return 401 when the wrong token is provided', async () => { - port = randomPort(); - server = new WebhookServer({ port, secret: 'correct-secret' }); + server = new WebhookServer({ port: randomPort(), secret: 'correct-secret' }); jest.spyOn(console, 'log').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const res = await makeRequest({ port, @@ -597,10 +596,10 @@ describe('WebhookServer', () => { }); it('should return 401 when Authorization header format is wrong', async () => { - port = randomPort(); - server = new WebhookServer({ port, secret: 'my-secret' }); + server = new WebhookServer({ port: randomPort(), secret: 'my-secret' }); jest.spyOn(console, 'log').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const res = await makeRequest({ port, @@ -613,10 +612,10 @@ describe('WebhookServer', () => { }); it('should accept requests with the correct Bearer token', async () => { - port = randomPort(); - server = new WebhookServer({ port, secret: 'valid-token' }); + server = new WebhookServer({ port: randomPort(), secret: 'valid-token' }); jest.spyOn(console, 'log').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const handler = jest.fn(); server.on('authed', handler); @@ -634,10 +633,10 @@ describe('WebhookServer', () => { }); it('should reject tokens of different length via timing-safe comparison', async () => { - port = randomPort(); - server = new WebhookServer({ port, secret: 'short' }); + server = new WebhookServer({ port: randomPort(), secret: 'short' }); jest.spyOn(console, 'log').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const res = await makeRequest({ port, @@ -654,11 +653,11 @@ describe('WebhookServer', () => { describe('custom webhook path', () => { it('should accept events on custom path', async () => { - port = randomPort(); - server = new WebhookServer({ port, path: '/custom/hooks' }); + server = new WebhookServer({ port: randomPort(), path: '/custom/hooks' }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const handler = jest.fn(); server.on('test', handler); @@ -675,11 +674,11 @@ describe('WebhookServer', () => { }); it('should return 404 on default path when custom path is configured', async () => { - port = randomPort(); - server = new WebhookServer({ port, path: '/custom/hooks' }); + server = new WebhookServer({ port: randomPort(), path: '/custom/hooks' }); jest.spyOn(console, 'log').mockImplementation(); jest.spyOn(console, 'warn').mockImplementation(); await server.start(); + port = parseInt(new URL(server.getUrl()).port); const res = await makeRequest({ port, diff --git a/src/webhook-server.ts b/src/webhook-server.ts index be3451b..0f4b5dd 100644 --- a/src/webhook-server.ts +++ b/src/webhook-server.ts @@ -2,6 +2,7 @@ import { timingSafeEqual } from 'crypto'; import express from 'express'; import type { Express, Request, Response, NextFunction } from 'express'; import { createServer, Server } from 'http'; +import type { AddressInfo } from 'net'; export interface WebhookEvent> { type: string; @@ -124,6 +125,7 @@ export class WebhookServer { this.server = createServer(this.app); this.server.on('error', reject); this.server.listen(this.config.port, () => { + this.config.port = (this.server!.address() as AddressInfo).port; console.log(`Webhook server listening on port ${this.config.port}`); console.log(`Webhook endpoint: http://localhost:${this.config.port}${this.config.path}`); resolve();