diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22d7bda..798f4b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [18, 20, 22] + node-version: [20, 22] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4f3df92 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,223 @@ +name: Release + +on: + workflow_dispatch: + inputs: + bump: + description: "patch: bug fixes (0.1.0→0.1.1) | minor: new features (0.1.0→0.2.0) | major: breaking changes (0.1.0→1.0.0)" + type: choice + default: "patch" + options: + - patch + - minor + - major + release_core: + description: "Release @mondaycom/hatcha-core" + type: boolean + default: true + release_server: + description: "Release @mondaycom/hatcha-server" + type: boolean + default: true + release_react: + description: "Release @mondaycom/hatcha-react" + type: boolean + default: true + +permissions: + contents: write + id-token: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: pnpm + registry-url: "https://registry.npmjs.org" + + - run: pnpm install --frozen-lockfile + + - name: Validate selection + env: + RELEASE_CORE: ${{ inputs.release_core }} + RELEASE_SERVER: ${{ inputs.release_server }} + RELEASE_REACT: ${{ inputs.release_react }} + run: | + if [[ "$RELEASE_CORE" != "true" && \ + "$RELEASE_SERVER" != "true" && \ + "$RELEASE_REACT" != "true" ]]; then + echo "::error::No packages selected for release" + exit 1 + fi + if [[ "$RELEASE_CORE" != "true" ]]; then + if [[ "$RELEASE_SERVER" == "true" || "$RELEASE_REACT" == "true" ]]; then + echo "::error::Cannot release server/react without core. Core must be included to ensure dependency versions are correct." + exit 1 + fi + fi + + - name: Bump versions + id: versions + env: + BUMP: ${{ inputs.bump }} + RELEASE_CORE: ${{ inputs.release_core }} + RELEASE_SERVER: ${{ inputs.release_server }} + RELEASE_REACT: ${{ inputs.release_react }} + run: | + if [[ "$RELEASE_CORE" == "true" ]]; then + cd packages/core + NEW_VERSION=$(npm version "$BUMP" --no-git-tag-version) + echo "core_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + echo "Core bumped to ${NEW_VERSION}" + cd ../.. + fi + + if [[ "$RELEASE_SERVER" == "true" ]]; then + cd packages/server + NEW_VERSION=$(npm version "$BUMP" --no-git-tag-version) + echo "server_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + echo "Server bumped to ${NEW_VERSION}" + cd ../.. + fi + + if [[ "$RELEASE_REACT" == "true" ]]; then + cd packages/react + NEW_VERSION=$(npm version "$BUMP" --no-git-tag-version) + echo "react_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT" + echo "React bumped to ${NEW_VERSION}" + cd ../.. + fi + + - name: Update lockfile + run: pnpm install --no-frozen-lockfile + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test + + - name: Typecheck + run: pnpm typecheck + + - name: Publish core + if: inputs.release_core + run: pnpm --filter @mondaycom/hatcha-core publish --no-git-checks --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish server + if: inputs.release_server + run: pnpm --filter @mondaycom/hatcha-server publish --no-git-checks --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish react + if: inputs.release_react + run: pnpm --filter @mondaycom/hatcha-react publish --no-git-checks --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Commit and tag + env: + CORE_VERSION: ${{ steps.versions.outputs.core_version }} + SERVER_VERSION: ${{ steps.versions.outputs.server_version }} + REACT_VERSION: ${{ steps.versions.outputs.react_version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add packages/*/package.json pnpm-lock.yaml + + MSG="release [skip ci]:" + TAGS=() + + if [[ -n "$CORE_VERSION" ]]; then + MSG="${MSG} core@${CORE_VERSION}" + TAGS+=("@mondaycom/hatcha-core@${CORE_VERSION}") + fi + if [[ -n "$SERVER_VERSION" ]]; then + MSG="${MSG} server@${SERVER_VERSION}" + TAGS+=("@mondaycom/hatcha-server@${SERVER_VERSION}") + fi + if [[ -n "$REACT_VERSION" ]]; then + MSG="${MSG} react@${REACT_VERSION}" + TAGS+=("@mondaycom/hatcha-react@${REACT_VERSION}") + fi + + git commit -m "${MSG}" + + for TAG in "${TAGS[@]}"; do + git tag "${TAG}" + done + + git push origin HEAD --tags --atomic + + - name: Create GitHub Releases + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CORE_VERSION: ${{ steps.versions.outputs.core_version }} + SERVER_VERSION: ${{ steps.versions.outputs.server_version }} + REACT_VERSION: ${{ steps.versions.outputs.react_version }} + run: | + LAST_TAG="" + + if [[ -n "$CORE_VERSION" ]]; then + LAST_TAG="@mondaycom/hatcha-core@${CORE_VERSION}" + gh release create "$LAST_TAG" \ + --title "hatcha-core ${CORE_VERSION}" \ + --generate-notes \ + --latest=false + fi + if [[ -n "$SERVER_VERSION" ]]; then + LAST_TAG="@mondaycom/hatcha-server@${SERVER_VERSION}" + gh release create "$LAST_TAG" \ + --title "hatcha-server ${SERVER_VERSION}" \ + --generate-notes \ + --latest=false + fi + if [[ -n "$REACT_VERSION" ]]; then + LAST_TAG="@mondaycom/hatcha-react@${REACT_VERSION}" + gh release create "$LAST_TAG" \ + --title "hatcha-react ${REACT_VERSION}" \ + --generate-notes \ + --latest=false + fi + + # Mark the last release as "Latest" + if [[ -n "$LAST_TAG" ]]; then + gh release edit "$LAST_TAG" --latest + fi + + - name: Summary + env: + CORE_VERSION: ${{ steps.versions.outputs.core_version }} + SERVER_VERSION: ${{ steps.versions.outputs.server_version }} + REACT_VERSION: ${{ steps.versions.outputs.react_version }} + run: | + echo "## Release Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Package | Version |" >> "$GITHUB_STEP_SUMMARY" + echo "|---------|---------|" >> "$GITHUB_STEP_SUMMARY" + if [[ -n "$CORE_VERSION" ]]; then + echo "| @mondaycom/hatcha-core | ${CORE_VERSION} |" >> "$GITHUB_STEP_SUMMARY" + fi + if [[ -n "$SERVER_VERSION" ]]; then + echo "| @mondaycom/hatcha-server | ${SERVER_VERSION} |" >> "$GITHUB_STEP_SUMMARY" + fi + if [[ -n "$REACT_VERSION" ]]; then + echo "| @mondaycom/hatcha-react | ${REACT_VERSION} |" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..744ca17 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd40910..7fb5cd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ pnpm test ## Code Style - TypeScript throughout, strict mode. -- No external runtime dependencies in `@hatcha/core`. +- No external runtime dependencies in `@mondaycom/hatcha-core`. - Keep bundle sizes minimal — the library should stay lightweight. ## License diff --git a/README.md b/README.md index 0cef88a..91710b8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

- npm + npm License CI

@@ -28,14 +28,14 @@ HATCHA (**H**yperfast **A**gent **T**est for **C**omputational **H**euristic **A ### 1. Install ```bash -npm install @mondaydotcomorg/hatcha-react @mondaydotcomorg/hatcha-server +npm install @mondaycom/hatcha-react @mondaycom/hatcha-server ``` ### 2. Add the API route ```typescript // app/api/hatcha/[...hatcha]/route.ts -import { createHatchaHandler } from "@mondaydotcomorg/hatcha-server/nextjs"; +import { createHatchaHandler } from "@mondaycom/hatcha-server/nextjs"; const handler = createHatchaHandler({ secret: process.env.HATCHA_SECRET!, @@ -49,8 +49,8 @@ export const POST = handler; ```tsx // app/layout.tsx -import { HatchaProvider } from "@mondaydotcomorg/hatcha-react"; -import "@mondaydotcomorg/hatcha-react/styles.css"; +import { HatchaProvider } from "@mondaycom/hatcha-react"; +import "@mondaycom/hatcha-react/styles.css"; export default function RootLayout({ children }) { return ( @@ -67,7 +67,7 @@ export default function RootLayout({ children }) { ```tsx "use client"; -import { useHatcha } from "@mondaydotcomorg/hatcha-react"; +import { useHatcha } from "@mondaycom/hatcha-react"; function AgentModeButton() { const { requestVerification } = useHatcha(); @@ -133,7 +133,7 @@ The answer **never** reaches the client. The signed token is opaque and contains ## Custom challenges ```typescript -import { registerChallenge } from "@mondaydotcomorg/hatcha-server"; +import { registerChallenge } from "@mondaycom/hatcha-server"; registerChallenge({ type: "hex", @@ -176,7 +176,7 @@ Pass `theme="dark"`, `theme="light"`, or `theme="auto"` to `` or ```typescript import express from "express"; -import { hatchaRouter } from "@mondaydotcomorg/hatcha-server/express"; +import { hatchaRouter } from "@mondaycom/hatcha-server/express"; const app = express(); app.use(express.json()); @@ -189,9 +189,9 @@ app.listen(3000); | Package | Description | |---------|-------------| -| [`@mondaydotcomorg/hatcha-core`](./packages/core) | Challenge generation and cryptographic verification | -| [`@mondaydotcomorg/hatcha-react`](./packages/react) | React component, provider, and styles | -| [`@mondaydotcomorg/hatcha-server`](./packages/server) | Next.js and Express server handlers | +| [`@mondaycom/hatcha-core`](./packages/core) | Challenge generation and cryptographic verification | +| [`@mondaycom/hatcha-react`](./packages/react) | React component, provider, and styles | +| [`@mondaycom/hatcha-server`](./packages/server) | Next.js and Express server handlers | ## Development diff --git a/examples/nextjs-app/app/api/hatcha/challenge/route.ts b/examples/nextjs-app/app/api/hatcha/challenge/route.ts index a5982f7..46c6b87 100644 --- a/examples/nextjs-app/app/api/hatcha/challenge/route.ts +++ b/examples/nextjs-app/app/api/hatcha/challenge/route.ts @@ -1,4 +1,4 @@ -import { createChallenge } from "@mondaydotcomorg/hatcha-core"; +import { createChallenge } from "@mondaycom/hatcha-core"; export async function GET() { const payload = await createChallenge({ diff --git a/examples/nextjs-app/app/api/hatcha/verify/route.ts b/examples/nextjs-app/app/api/hatcha/verify/route.ts index f361e38..36adbdd 100644 --- a/examples/nextjs-app/app/api/hatcha/verify/route.ts +++ b/examples/nextjs-app/app/api/hatcha/verify/route.ts @@ -1,4 +1,4 @@ -import { verifyAnswer } from "@mondaydotcomorg/hatcha-core"; +import { verifyAnswer } from "@mondaycom/hatcha-core"; export async function POST(request: Request) { const body = await request.json(); diff --git a/examples/nextjs-app/app/layout.tsx b/examples/nextjs-app/app/layout.tsx index fb34c3c..8d00968 100644 --- a/examples/nextjs-app/app/layout.tsx +++ b/examples/nextjs-app/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next"; -import { HatchaProvider } from "@mondaydotcomorg/hatcha-react"; -import "@mondaydotcomorg/hatcha-react/styles.css"; +import { HatchaProvider } from "@mondaycom/hatcha-react"; +import "@mondaycom/hatcha-react/styles.css"; import "./globals.css"; export const metadata: Metadata = { diff --git a/examples/nextjs-app/app/page.tsx b/examples/nextjs-app/app/page.tsx index da5e1d7..9caf12f 100644 --- a/examples/nextjs-app/app/page.tsx +++ b/examples/nextjs-app/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { useHatcha } from "@mondaydotcomorg/hatcha-react"; +import { useHatcha } from "@mondaycom/hatcha-react"; export default function Home() { const { requestVerification } = useHatcha(); diff --git a/examples/nextjs-app/package.json b/examples/nextjs-app/package.json index f1c9b4e..6a84f35 100644 --- a/examples/nextjs-app/package.json +++ b/examples/nextjs-app/package.json @@ -8,9 +8,9 @@ "start": "next start" }, "dependencies": { - "@mondaydotcomorg/hatcha-core": "workspace:*", - "@mondaydotcomorg/hatcha-react": "workspace:*", - "@mondaydotcomorg/hatcha-server": "workspace:*", + "@mondaycom/hatcha-core": "workspace:*", + "@mondaycom/hatcha-react": "workspace:*", + "@mondaycom/hatcha-server": "workspace:*", "next": "^15", "react": "^19", "react-dom": "^19" diff --git a/packages/core/package.json b/packages/core/package.json index 6af846b..05dd74b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,5 +1,5 @@ { - "name": "@mondaydotcomorg/hatcha-core", + "name": "@mondaycom/hatcha-core", "version": "0.1.0", "description": "Challenge generation and cryptographic verification for HATCHA", "type": "module", diff --git a/packages/core/src/__tests__/e2e.test.ts b/packages/core/src/__tests__/e2e.test.ts index 82a8f4d..6a1d09c 100644 --- a/packages/core/src/__tests__/e2e.test.ts +++ b/packages/core/src/__tests__/e2e.test.ts @@ -19,7 +19,7 @@ describe("createChallenge", () => { expect(typeof challenge.prompt).toBe("string"); expect(typeof challenge.timeLimit).toBe("number"); expect(challenge.timeLimit).toBeGreaterThan(0); - expect((challenge as Record).answer).toBeUndefined(); + expect("answer" in challenge).toBe(false); expect(typeof token).toBe("string"); expect(token).toContain("."); }); diff --git a/packages/core/src/crypto.ts b/packages/core/src/crypto.ts index a3a2fb2..8f54aa3 100644 --- a/packages/core/src/crypto.ts +++ b/packages/core/src/crypto.ts @@ -69,20 +69,14 @@ export async function verifyToken( const [payloadB64, sigB64] = parts; - let payloadBytes: Uint8Array; - let sigBytes: Uint8Array; try { - payloadBytes = Uint8Array.from(atob(payloadB64), (c) => c.charCodeAt(0)); - sigBytes = Uint8Array.from(atob(sigB64), (c) => c.charCodeAt(0)); - } catch { - return null; - } + const payloadBytes = Uint8Array.from(atob(payloadB64), (c) => c.charCodeAt(0)); + const sigBytes = Uint8Array.from(atob(sigB64), (c) => c.charCodeAt(0)); - const key = await getKey(secret); - const valid = await crypto.subtle.verify("HMAC", key, sigBytes, payloadBytes); - if (!valid) return null; + const key = await getKey(secret); + const valid = await crypto.subtle.verify("HMAC", key, sigBytes, payloadBytes); + if (!valid) return null; - try { return JSON.parse(new TextDecoder().decode(payloadBytes)) as TokenPayload; } catch { return null; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3d82f7e..d069f3a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,5 @@ /* ------------------------------------------------------------------ */ -/* Public types for @mondaydotcomorg/hatcha-core */ +/* Public types for @mondaycom/hatcha-core */ /* ------------------------------------------------------------------ */ /** What the client sees — never includes the answer. */ diff --git a/packages/react/package.json b/packages/react/package.json index eb603f7..0e28a99 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,5 +1,5 @@ { - "name": "@mondaydotcomorg/hatcha-react", + "name": "@mondaycom/hatcha-react", "version": "0.1.0", "description": "React component for HATCHA — reverse CAPTCHA for agents", "type": "module", @@ -24,7 +24,7 @@ "clean": "rm -rf dist" }, "dependencies": { - "@mondaydotcomorg/hatcha-core": "workspace:*" + "@mondaycom/hatcha-core": "workspace:*" }, "peerDependencies": { "react": "^18 || ^19", diff --git a/packages/react/src/hatcha.tsx b/packages/react/src/hatcha.tsx index 0acc135..e2677f4 100644 --- a/packages/react/src/hatcha.tsx +++ b/packages/react/src/hatcha.tsx @@ -7,7 +7,7 @@ import { useRef, type KeyboardEvent, } from "react"; -import type { ChallengeDisplay } from "@mondaydotcomorg/hatcha-core"; +import type { ChallengeDisplay } from "@mondaycom/hatcha-core"; /* ------------------------------------------------------------------ */ /* Props */ diff --git a/packages/server/package.json b/packages/server/package.json index 4378199..d3ad588 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,5 +1,5 @@ { - "name": "@mondaydotcomorg/hatcha-server", + "name": "@mondaycom/hatcha-server", "version": "0.1.0", "description": "Server middleware for HATCHA — Next.js and Express handlers", "type": "module", @@ -34,7 +34,7 @@ "test": "vitest run" }, "dependencies": { - "@mondaydotcomorg/hatcha-core": "workspace:*" + "@mondaycom/hatcha-core": "workspace:*" }, "devDependencies": { "@types/express": "^5", diff --git a/packages/server/src/__tests__/handler.test.ts b/packages/server/src/__tests__/handler.test.ts index b6daab7..8251b7b 100644 --- a/packages/server/src/__tests__/handler.test.ts +++ b/packages/server/src/__tests__/handler.test.ts @@ -11,7 +11,7 @@ describe("handleChallenge", () => { expect(typeof result.challenge.type).toBe("string"); expect(typeof result.challenge.prompt).toBe("string"); expect(typeof result.token).toBe("string"); - expect((result.challenge as Record).answer).toBeUndefined(); + expect("answer" in result.challenge).toBe(false); }); it("respects challengeTypes option", async () => { @@ -48,7 +48,7 @@ describe("handleVerify", () => { it("returns error when answer is missing", async () => { const result = await handleVerify(config, { token: "some-token" }); expect(result.success).toBe(false); - expect((result as Record).error).toBe( + expect("error" in result && result.error).toBe( "Missing answer or token.", ); }); @@ -56,7 +56,7 @@ describe("handleVerify", () => { it("returns error when token is missing", async () => { const result = await handleVerify(config, { answer: "some-answer" }); expect(result.success).toBe(false); - expect((result as Record).error).toBe( + expect("error" in result && result.error).toBe( "Missing answer or token.", ); }); diff --git a/packages/server/src/express.ts b/packages/server/src/express.ts index 5a635c1..7737af9 100644 --- a/packages/server/src/express.ts +++ b/packages/server/src/express.ts @@ -10,7 +10,7 @@ import { * Usage: * * import express from "express"; - * import { hatchaRouter } from "@mondaydotcomorg/hatcha-server/express"; + * import { hatchaRouter } from "@mondaycom/hatcha-server/express"; * * const app = express(); * app.use(express.json()); diff --git a/packages/server/src/handler.ts b/packages/server/src/handler.ts index 00f2b87..b7cbb0f 100644 --- a/packages/server/src/handler.ts +++ b/packages/server/src/handler.ts @@ -2,7 +2,7 @@ import { createChallenge, verifyAnswer, type HatchaConfig, -} from "@mondaydotcomorg/hatcha-core"; +} from "@mondaycom/hatcha-core"; export interface HatchaServerConfig { /** HMAC secret for signing challenge tokens. */ diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 3c3e8d3..885bcaf 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -3,9 +3,9 @@ export type { HatchaServerConfig } from "./handler.js"; export { createHatchaHandler } from "./nextjs.js"; export { hatchaRouter } from "./express.js"; -export { registerChallenge, getGenerators } from "@mondaydotcomorg/hatcha-core"; +export { registerChallenge, getGenerators } from "@mondaycom/hatcha-core"; export type { ChallengeGenerator, ChallengeDisplay, HatchaConfig, -} from "@mondaydotcomorg/hatcha-core"; +} from "@mondaycom/hatcha-core"; diff --git a/packages/server/src/nextjs.ts b/packages/server/src/nextjs.ts index 8ea9a8e..cf7a107 100644 --- a/packages/server/src/nextjs.ts +++ b/packages/server/src/nextjs.ts @@ -9,7 +9,7 @@ import { * * Usage (app/api/hatcha/[...hatcha]/route.ts): * - * import { createHatchaHandler } from "@mondaydotcomorg/hatcha-server/nextjs"; + * import { createHatchaHandler } from "@mondaycom/hatcha-server/nextjs"; * * const handler = createHatchaHandler({ * secret: process.env.HATCHA_SECRET!, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20de558..408eb09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,13 +20,13 @@ importers: examples/nextjs-app: dependencies: - '@mondaydotcomorg/hatcha-core': + '@mondaycom/hatcha-core': specifier: workspace:* version: link:../../packages/core - '@mondaydotcomorg/hatcha-react': + '@mondaycom/hatcha-react': specifier: workspace:* version: link:../../packages/react - '@mondaydotcomorg/hatcha-server': + '@mondaycom/hatcha-server': specifier: workspace:* version: link:../../packages/server next: @@ -60,7 +60,7 @@ importers: packages/react: dependencies: - '@mondaydotcomorg/hatcha-core': + '@mondaycom/hatcha-core': specifier: workspace:* version: link:../core devDependencies: @@ -85,7 +85,7 @@ importers: packages/server: dependencies: - '@mondaydotcomorg/hatcha-core': + '@mondaycom/hatcha-core': specifier: workspace:* version: link:../core devDependencies: