{"owner":"dubinc","repo":"dub","hasSkills":true,"totalSkillsCount":13,"totalTokensCount":6182,"categories":["anthropic-skill","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/jobs-define-job/SKILL.md",".agents/skills/playwright-api-tests/SKILL.md","packages/cli/README.md","packages/embeds/core/README.md","packages/embeds/react/README.md","packages/hubspot-app/CLAUDE.md","packages/hubspot-app/README.md","packages/stripe-app/README.md","packages/tinybird/README.md","packages/ui/README.md","packages/ui/src/icons/nucleo/README.md","packages/utils/README.md","packages/workers/r2-object-created/README.md"],"skills":{".agents/skills/jobs-define-job/SKILL.md":"---\nname: jobs-define-job\ndescription: Add or migrate background jobs with defineJob, register them in jobLoaders, and dispatch via job.dispatch/dispatchBatch instead of qstash.publishJSON to a cron URL. Use when adding a QStash worker, converting a POST /api/cron route into a job, or when the user mentions defineJob, job handlers, /api/jobs/process, or background jobs.\n---\n\n# Background jobs: use defineJob\n\nPayload-driven QStash work goes through `defineJob`. Do **not** add a new HTTP route under `/api/jobs` — every job is executed by the existing [`apps/web/app/api/jobs/process/[jobName]/route.ts`](apps/web/app/api/jobs/process/[jobName]/route.ts).\n\nKeep using `withCron` for Vercel GET schedules and for cron scanners that have no job envelope. See the `cron-use-with-cron` skill for those.\n\n## File layout\n\n```\napps/web/lib/jobs/\n├── index.ts                 # defineJob — do not edit unless changing the framework\n├── registry.ts              # jobLoaders — register every new job here\n├── send-jobs.ts             # envelope + QStash request builder\n└── handlers/\n    └── {name}-job.ts        # one file per job\n```\n\n## 1. Create the handler\n\nAdd `apps/web/lib/jobs/handlers/{name}-job.ts`. The `name` must be kebab-case and end in `-job` (enforced by `jobNameSchema`: `/^[a-z][a-z0-9]*(-[a-z0-9]+)*-job$/`).\n\n```ts\nimport * as z from \"zod/v4\";\nimport { defineJob } from \"../index\";\n\nconst inputSchema = z.object({\n  programId: z.string(),\n  partnerId: z.string(),\n});\n\nexport const unbanPartnerJob = defineJob({\n  name: \"unban-partner-job\",\n  schema: inputSchema,\n  defaults: {\n    retries: 3, // optional; QStash retries on 5xx\n    // queue: \"unban-partner\", // optional; named QStash queue\n    // flowControl: { key: \"unban-partner\", parallelism: 20 },\n  },\n  async handle(input) {\n    // skip (permanent / not found) → return (process route returns 2xx, QStash does not retry)\n    // transient failure → throw (process route returns 500, QStash retries)\n  },\n});\n```\n\nExport the const as camelCase `{name}Job` matching the kebab `name`.\n\nReference handlers:\n\n- Simple skip/work: `unban-partner-job.ts`, `create-tremendous-campaign-job.ts`\n- Self-pagination: `folder-deleted-job.ts`, `domain-deleted-job.ts`, `partner-search-sync-job.ts`\n- `defaults.flowControl`: `partner-search-sync-job.ts`\n\n## 2. Register it\n\nAdd a **static** `import()` in [`apps/web/lib/jobs/registry.ts`](apps/web/lib/jobs/registry.ts) `jobLoaders`. The object key must equal `defineJob({ name })`. Webpack code-splits each handler.\n\n```ts\n\"unban-partner-job\": () =>\n  import(\"./handlers/unban-partner-job\").then((m) => m.unbanPartnerJob),\n```\n\nDo not register by editing the process route. `loadJob` throws if `job.name !==` the registry key.\n\n## 3. Dispatch from call sites\n\nImport the handler (not `qstash`) and call `dispatch` / `dispatchBatch`:\n\n```ts\nimport { unbanPartnerJob } from \"@/lib/jobs/handlers/unban-partner-job\";\n\nawait unbanPartnerJob.dispatch(\n  { workspaceId, programId, partnerId },\n  { label: partnerId },\n);\n\nawait folderDeletedJob.dispatchBatch(\n  folderIds.map((folderId) => ({ folderId })),\n  ({ folderId }) => ({ label: folderId }),\n);\n```\n\nPer-dispatch options merge over `defaults`: `delay`, `notBefore`, `deduplicationId`, `retries`, `queue`, `flowControl`, `label`.\n\nFailed QStash publish is persisted to the jobs outbox automatically — do not catch-and-swallow unless even the outbox persist failing must not fail the source mutation (see `queue-partner-search-sync.ts`).\n\nSelf-pagination: call `theJob.dispatch(nextPayload, { delay: 1 })` from `handle` (see `folder-deleted-job`, `partner-search-sync-job`).\n\nDo not call `job.execute` from app code. That is only for the process route (and cron drain shims below).\n\n## 4. Convert an existing cron worker\n\n1. Move the `withCron` body into `handle`. Replace `logAndRespond(\"skip…\")` with `console.info` / `console.error` + `return`.\n2. Register + switch every `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` targeting that cron URL to `job.dispatch` / `dispatchBatch`.\n3. Keep a thin POST shim at the old `/api/cron/...` URL that parses the **old** body (not the job envelope) and calls `job.execute(payload)`. That drains in-flight QStash messages. Do not add a shim for brand-new jobs.\n4. Delete cron-only helpers that moved with the handler.\n\n## Handle semantics\n\n| Outcome | What to do | HTTP from process route | QStash |\n| --- | --- | --- | --- |\n| Work done | `return` | 200 | stop |\n| Skip (not found, already done, env not configured) | `console.*` + `return` | 200 | stop |\n| Bad payload | throw `ZodError` (schema.parse) | 200 | stop (non-retryable) |\n| Transient failure | `throw` | 500 | retry |\n\nUnknown job names and invalid envelopes also return 2xx so QStash does not retry forever.\n\n## Do not use defineJob for\n\n- Vercel GET crons in `apps/web/vercel.json`\n- Scanners that only fan out work (the worker they enqueue can be a job)\n- Importers that republish continuation state to the same URL\n- `/api/cron/queue/retry` (job replay infrastructure)\n- Path-param identity (`/api/cron/links/[linkId]/…`) unless the id moves into the payload\n- Outbound webhook forwarding and postbacks\n\n## Do not\n\n- Add a new `/api/jobs/...` route or a new `/api/cron/...` POST worker for payload-driven work.\n- Call `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` with `/api/jobs/process/...` — use `dispatch`.\n- Register jobs with a dynamic `import()` that webpack cannot statically analyze, or a key that differs from `defineJob({ name })`.\n- Name a job without the `-job` suffix.\n- Use `job.execute` at dispatch call sites.\n- Run `pnpm build` after adding a job.\n",".agents/skills/playwright-api-tests/SKILL.md":"---\nname: playwright-api-tests\ndescription: Add or extend Playwright HTTP API specs under apps/web/playwright/api using the shared api/workspace fixtures and Bearer auth from globalSetup. Use when writing Playwright API tests, e2e API specs, or testing /api routes with the api project (not Vitest under apps/web/tests, not browser UI e2e).\n---\n\n# Playwright API tests\n\nHTTP API coverage lives in `apps/web/playwright/api/<resource>/*.spec.ts`. Auth is seeded once by Playwright `globalSetup` (`apps/web/global-setup.ts` → `setup-test-workspace.ts`) into `playwright/.auth/api.json`. Specs use the custom `test` from `../fixtures` — not `@playwright/test` directly for `test`.\n\nDo **not** put these under `apps/web/tests/` (Vitest) or under `playwright/partners` / `playwright/workspaces` (browser e2e).\n\n## Before writing\n\n1. Find the route handler and response shape (Zod schemas under `apps/web/lib/zod`, types under `@/lib/types` or Prisma).\n2. Prefer mirroring an existing nearby resource folder: `tags/`, `folders/`, `customers/`, `workspaces/`.\n3. Confirm the endpoint is workspace-scoped Bearer API (most `/api/*` routes). The fixture already sends `Authorization: Bearer …`.\n\n## What to cover\n\nBefore considering an endpoint covered, ask:\n\n- What happens with valid input?\n- What happens with invalid input?\n- Who is allowed to call it?\n- Can another workspace access the resource?\n- What happens when the resource does not exist?\n- What happens with duplicate data?\n- What happens at boundary values?\n- What state does the operation leave behind?\n- Can the operation be safely retried?\n- Does pagination/filtering/sorting behave correctly?\n- What happens when multiple requests happen concurrently?\n- Are errors returned in the documented format?\n\nNot every endpoint needs a test for every item. The goal is to cover the actual failure modes and API contract, not to maximize the number of tests.\n\n## File layout\n\n```\napps/web/playwright/api/\n├── fixtures.ts              # api + workspace fixtures (reuse; don't duplicate)\n├── setup-test-workspace.ts  # globalSetup seed only — extend only if auth/workspace setup must change\n└── <resource>/\n    ├── <resource>.spec.ts\n    └── <resource>-pagination.spec.ts  # optional split for heavy pagination suites\n```\n\nOne folder per resource. Name files `kebab-case.spec.ts` (`tags/tags.spec.ts`, `customers/customers-pagination.spec.ts`).\n\n## Spec template\n\n```ts\nimport { expect } from \"@playwright/test\";\nimport { randomName } from \"../../utils\";\nimport { test, type ApiClient } from \"../fixtures\";\n\nasync function createThing(\n  api: ApiClient,\n  overrides: Record<string, unknown> = {},\n) {\n  return api.post<YourType>(\"/api/things\", {\n    name: randomName(\"thing\"),\n    ...overrides,\n  });\n}\n\nasync function deleteThing(api: ApiClient, id: string | undefined) {\n  if (!id) return;\n  await api.delete(`/api/things/${id}`);\n}\n\ntest(\"POST /things\", async ({ api }) => {\n  let id: string | undefined;\n\n  try {\n    const body = { name: randomName(\"thing\") };\n    const { status, data } = await api.post<YourType>(\"/api/things\", body);\n    id = data.id;\n\n    expect(status).toEqual(201);\n    expect(data).toStrictEqual({\n      id: expect.any(String),\n      ...body,\n      // stable nulls / defaults from the API\n    });\n  } finally {\n    await deleteThing(api, id);\n  }\n});\n```\n\n## Required conventions\n\n| Rule                                            | Detail                                                                                                        |\n| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| Import `test` from `../fixtures`                | Provides `api`, `workspace`, and `program`                                                                    |\n| Serial only when tests share state              | API project is `fullyParallel: true`. Do not add `mode: \"parallel\"`. Use `test.describe.configure({ mode: \"serial\" })` only when tests in a file/describe share state (e.g. domains, seeded pagination) |\n| Cleanup in `finally`                            | Create → assert → always delete created rows                                                                  |\n| Unique names/ids                                | Use `randomName` / `randomCustomer` / `randomPartnerEmail` from `../../utils` — never fixed colliding names   |\n| Assert status + body                            | Prefer `toStrictEqual` / `toEqual` on full shapes; use `expect.any(String)` for ids/timestamps                |\n| Default shape once                              | Happy-path POST owns the full default resource (and nested) shape. Variant tests assert only what they change |\n| HTTP contract only                              | Assert status + JSON. Do not poll/sleep for `waitUntil`, R2, or other background jobs — CI has no `STORAGE_*` |\n| Error responses                                 | Match `{ error: { code, message, doc_url } }` exactly                                                         |\n| Typed generics                                  | `api.get<T>`, `api.post<T>`, etc.                                                                             |\n| Seeded fixtures                                 | `{ workspace }`, `{ program }` (`id`, `defaultGroupId`), `TEST_WORKSPACE` — not Vitest `E2E_*` constants      |\n\n### Fixtures (`api` / `workspace` / `program`)\n\n- `api.get/post/patch/delete(url, data?)` → `{ status, data }` (JSON parsed).\n- Paths are app-relative (`/api/...`); `baseURL` is `http://localhost:8888`.\n- `workspace.id` / `workspace.slug` and `program.id` / `program.defaultGroupId` come from `.auth/api.json`.\n- Seeded program domain/url: `TEST_WORKSPACE.program` in `setup-test-workspace.ts`.\n\n### Helpers\n\n- Shared randomizers / sort assertions: `apps/web/playwright/utils.ts`.\n- Local `createX` / `deleteX` helpers in the spec file when the same setup repeats.\n- Once `createX` exists, use it for setup **and** follow-up creates (pass overrides for identity fields that must stay stable, e.g. `email`). Happy-path tests may still inline `api.post` so the asserted body is local.\n- Prisma direct seed is allowed for bulk fixtures (see `customers/customers-pagination.spec.ts`); still clean up in `finally` / after hooks. No DELETE route → Prisma/`conn` cleanup is fine (`partners/partners.spec.ts`).\n\n### Error / table-driven cases\n\n```ts\nconst errorCases = [\n  {\n    name: \"POST /things – missing name\",\n    body: {},\n    expected: {\n      status: 422,\n      data: {\n        error: {\n          code: \"unprocessable_entity\",\n          message: \"…\",\n          doc_url:\n            \"https://dub.co/docs/api-reference/errors#unprocessable-entity\",\n        },\n      },\n    },\n  },\n];\n\nfor (const { name, body, expected } of errorCases) {\n  test(name, async ({ api }) => {\n    expect(await api.post(\"/api/things\", body)).toEqual(expected);\n  });\n}\n```\n\nOptional: validate richer payloads with Zod (`.parse(...)`) like `workspaces/workspaces.spec.ts` / enriched customer fields.\n\n## Migrating Vitest API tests\n\nWhen converting `apps/web/tests/<resource>/*.test.ts`:\n\n1. Add or extend `playwright/api/<resource>/<resource>.spec.ts` — do not leave a parallel Vitest HTTP spec.\n2. Map `IntegrationHarness` / `http.post({ path })` → `api` fixture (`/api/...` paths).\n3. Replace `E2E_*` / `E2E_PARTNER_GROUP` with `{ workspace }`, `{ program }`, and `TEST_WORKSPACE`.\n4. Delete the Vitest file after the Playwright spec covers its cases.\n\n## Do not\n\n- Call `setupTestWorkspace` from specs (only `globalSetup`).\n- Import browser `page` / storage-state auth for API specs.\n- Commit secrets or change the fixed Playwright token unless intentionally rotating local/CI test auth.\n- Skip cleanup (parallel API project will leak / flake).\n- Run `pnpm build` after adding tests.\n- Poll or `setTimeout` for R2/storage/`waitUntil` side effects (no `STORAGE_*` in CI). Assert the immediate JSON body instead (e.g. foreign `image` URLs stay `null` on create).\n- Copy Vitest `IntegrationHarness` or `E2E_*` constants into Playwright specs.\n\n## Run\n\nDev server on port 8888 (local), then:\n\n```sh\npnpm --filter web test:e2e --project=api\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts -g \"POST /tags\"\n```\n\n`-g` / `--grep` matches the test title (regex). Combine with a file path to narrow further.\nIn CI, `playwright.config.ts` starts `pnpm start -p 8888` via `webServer`.\n\nMore context: `apps/web/playwright/README.md` (API section).\n","packages/cli/README.md":"# `dub-cli`\n\nA CLI for easily shortening URLs with the [Dub API](https://dub.co/api).\n\nhttps://github.com/user-attachments/assets/2ce9fe51-68ab-4e6d-b08d-4da09c17f90e\n\n## Available Commands\n\n| Command                   | Description                                                                                                                                                                                              |\n| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `dub login`               | Log into the DUB platform                                                                                                                                                                                |\n| `dub config`              | See your configured workspace credentials                                                                                                                                                                |\n| `dub domains`             | Configure your workspace domain                                                                                                                                                                          |\n| `dub shorten [url] [key]` | Create a short link. You can preemptively pass the URL and the generated short link key, or go through the CLI prompts.                                                                                  |\n| `dub links [options]`     | Search for links in your Dub workspace. Available options include: `-s, --search <search>` to search for a link by name, or `-l, --limit <limit>` to limit the number of links returned (default is 10). |\n| `dub help [command]`      | Display help for a specific command                                                                                                                                                                      |\n\n## Running Locally for Development\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package in watch mode:\n   ```bash\n   pnpm dev\n   ```\n3. In a separate terminal, navigate to the `cli` folder again and run an [available command](#available-commands):\n   ```bash\n   pnpm start [command]\n   ```\n4. See all [available commands](#available-commands) and options:\n   ```bash\n   pnpm start help\n   ```\n\n## Testing Production-like Setup\n\n> **Warning**\n> If you have previously installed `dub-cli` globally, uninstall it first to avoid conflicts\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package:\n   ```bash\n   pnpm build\n   ```\n3. Link the package globally:\n   ```bash\n    npm link\n   ```\n4. Verify the installation:\n   ```bash\n    dub -v\n   ```\n5. Run commands:\n   ```bash\n   dub [command]\n   ```\n","packages/embeds/core/README.md":"# `@dub/embed-core`\n\n`@dub/embed-core` is a library that contains the core logic for embedding Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-core\n```\n\n## License\n\n[MIT](./LICENSE.md)\n","packages/embeds/react/README.md":"# `@dub/embed-react`\n\n`@dub/embed-react` is a library of React components that are used embed Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-react\n```\n\n## License\n\n[MIT](./LICENSE.md)\n","packages/hubspot-app/CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working on HubSpot components\n\nIMPORTANT: IF THE 'HubSpot' MCP SERVER IS INSTALLED USE THE TOOLS BEFORE TRYING TO MANUALLY USE CLI COMMANDS OR BEFORE TRYING TO DO ANYTHING WITH HUBSPOT ASSETS\n\n## HubSpot Project Information\n\n- The project configuration is in the `hsproject.json` file\n- A directory is considered a part of the project it or a directory above it contains a `hsproject.json` file\n- The project src directory is defined in the `srcDir` field in the `hsproject.json`\n- The project's platform version is defined in `platformVersion` in the `hs project.json`\n- The `platformVersion` determines what features the project has access to as well as the shape of the configuration files\n\n## npm packages\n\n### `@hubspot/ui-extensions`\n\n- In the `@hubspot/ui-extensions` npm package, only the component properties defined by the component are valid. `style` properties are not valid\n\n## Component Information\n\n### General\n\n- Component configuration files must end with `-hsmeta.json`\n- The `uid` field in the `-hsmeta.json` files must be unique with the project\n- The `type` field in the `-hsmeta.json` files defines the type of the component\n- Components can not be in nested subdirectories, only the specified directories in their corresponding component rules.\n- Example components can be found in https://github.com/HubSpot/hubspot-project-components. The directories are split up by platform version and follow this format `${platformVersion}/components`\n- All component subdirectories must be in the project source directory\n\n### app component\n\n- There can only be one `app` component\n- `app` component must be in the `app` directory\n- If the `config.distribution` field is set to `marketplace`, the only valid `config.auth.type` value is `oauth`\n\n### card\n\n- `card` components must be in the `app/cards` directory\n- The global `window` object is not available in the `card` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `card` components\n\n### app-event\n\n- `app-event` components must be in the `app/app-events` directory\n-\n\n### app-object\n\n- `app-object` components must be in the `app/app-object` directory\n\n### app-function\n\n- `app-function` components must be in the `app/functions` directory\n- `app-function` components are not available when `config.distribution` is set to `marketplace` in the `app` component `-hsmeta.son` file\n\n# settings\n\n- There can only be one `settings` component\n- `settings` components must be in the `app/settings` directory\n- The global `window` object is not available in the `settings` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `settings` components\n- React Components from `@hubspot/ui-extensions/crm` cannot be used in `settings` components\n\n# scim\n\n- There can only be one `scim` component\n- `scim` components must be in the `app/scim` directory\n\n# webhooks\n\n- There can only be one `webhooks` component.\n- `webhooks` components must be in the `app/webhooks` directory\n- `webhooks` components can only be in projects where `config.distribution` is private and `config.auth.type` is `static`\n\n### workflow-actions\n\n- `workflow-action` components must be in the `app/workflow-actions` directory\n\n## HubSpot CLI commands\n\n- All the commands and subcommands have a `--help` argument that provides details on the command and it's arguments\n- The help output is standard yargs output\n- The commands for working with projects in HubSpot are subcommands of `hs project`\n- Debugging flag that can be added to `hs` commands and subcommands: `--debug`\n- Debugging problems with CLI installation: `hs doctor`\n- `hs project open` will open the current project page in the browser\n- `hs init` is required to set up the hubspot configuration file\n- `hs auth` will authenticate a new account. This will require a user to open a browser and paste a token in a CLI prompt.\n- All the commands for managing HubSpot accounts in the CLI are subcommands of `hs account`\n\n## General\n\n- Follow existing patterns in the codebase\n- Use proper component structure based on component `type` in the `-hsmeta.json` file\n- Ensure configuration files follow HubSpot naming conventions\n- Always validate that components are placed in correct directories\n","packages/hubspot-app/README.md":"# Dub HubSpot App\n\nThis package contains the HubSpot app integration for Dub.\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Upload the project to HubSpot account\npnpm hs project upload --account=<account-id>\n\n# Set up a test environment and start local development\npnpm hs project dev\n\n# Install all project dependencies\npnpm hs project install-deps\n\n# Add new feature to app\npnpm hs project add\n\n# List all accounts connected\npnpm hs account list\n```\n","packages/stripe-app/README.md":"# Stripe App\n\nThis is the [Stripe app](https://marketplace.stripe.com/apps/dub-conversions) for Dub Conversions.\n\n## Publish new version\n\n1. Run `stripe login` in your terminal and sign in to `Dub Technologies, Inc.`.\n2. Navigate to the `packages/stripe-app` directory.\n3. Increment the `version` field in `stripe-app.json`.\n4. Upload the updated app using `stripe apps upload`.\n5. Publish the new version via the [Stripe dashboard](https://dashboard.stripe.com/apps/dub.co).\n\n## Run locally\n\n```\nstripe apps start\n```\n","packages/tinybird/README.md":"# Dub Tinybird Setup\n\n```bash\n├── datasources\n│   └── dub_click_events.datasource\n│   └── dub_click_events_mv.datasource\n│   └── dub_links_metadata.datasource\n│   └── dub_links_metadata_latest.datasource\n├── endpoints\n│   ├── browser.pipe\n│   ├── city.pipe\n│   ├── clicks.pipe\n│   ├── country.pipe\n│   ├── device.pipe\n│   ├── os.pipe\n│   └── referer.pipe\n│   └── timeseries.pipe\n│   └── top_links.pipe\n│   └── top_urls.pipe\n├── pipes\n│   ├── dub_click_events_pipe.pipe\n│   ├── dub_links_metadata_pipe.pipe\n```\n","packages/ui/README.md":"# `@dub/ui`\n\n`@dub/ui` is a library of React components that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/ui\n```\n","packages/ui/src/icons/nucleo/README.md":"# Nucleo Icons\n\n> _❗ These icons are copyrighted and redistribution is prohibited. See the full Nucleo copyright notice below._\n\nCopyright © Nucleo\n\nVersion 1.3, January 3, 2024\n\nNucleo Icons\n\nhttps://nucleoapp.com/\n\n- Redistribution of icons is prohibited.\n- Icons are restricted for use only within the product they are bundled with.\n\nFor more details:\n\nhttps://nucleoapp.com/license\n","packages/utils/README.md":"# `@dub/utils`\n\n`@dub/utils` is a library of utility functions that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/utils\n```\n","packages/workers/r2-object-created/README.md":"# r2-object-created Worker (thin forwarder)\n\nCloudflare Queue **push** consumer that forwards each R2 `object-create` notification to Next.js. Magic-byte validation, delete-on-mismatch, and Axiom logging run in the app.\n\n## Quick Edit\n\n1. Paste [`worker.js`](./worker.js) into Workers Quick Edit → Deploy.\n2. Settings → Bindings / Variables / Secrets (below).\n3. Confirm public R2 bucket **Event notification** `object-create` → matching queue.\n\n## Bindings\n\n| Type              | Name                               | Value                                                     |\n| ----------------- | ---------------------------------- | --------------------------------------------------------- |\n| Queues (consumer) | —                                  | `r2-object-created` or `-dev`                             |\n| Variable          | `DUB_WEBHOOK_URL`                  | `https://<host>/api/cloudflare/webhook/r2-object-created` |\n| Secret            | `CLOUDFLARE_WORKER_WEBHOOK_SECRET` | Same as Next.js `CLOUDFLARE_WORKER_WEBHOOK_SECRET`        |\n\nNo R2 or Axiom bindings on the Worker — Next.js uses `STORAGE_*` and Axiom already.\n\n## Flow\n\n```text\nR2 object-create → Queue → Worker → POST /api/cloudflare/webhook/r2-object-created\n  → magic-byte check → delete public object on mismatch → Axiom\n```\n"},"files":{".agents/skills/jobs-define-job/SKILL.md":"---\nname: jobs-define-job\ndescription: Add or migrate background jobs with defineJob, register them in jobLoaders, and dispatch via job.dispatch/dispatchBatch instead of qstash.publishJSON to a cron URL. Use when adding a QStash worker, converting a POST /api/cron route into a job, or when the user mentions defineJob, job handlers, /api/jobs/process, or background jobs.\n---\n\n# Background jobs: use defineJob\n\nPayload-driven QStash work goes through `defineJob`. Do **not** add a new HTTP route under `/api/jobs` — every job is executed by the existing [`apps/web/app/api/jobs/process/[jobName]/route.ts`](apps/web/app/api/jobs/process/[jobName]/route.ts).\n\nKeep using `withCron` for Vercel GET schedules and for cron scanners that have no job envelope. See the `cron-use-with-cron` skill for those.\n\n## File layout\n\n```\napps/web/lib/jobs/\n├── index.ts                 # defineJob — do not edit unless changing the framework\n├── registry.ts              # jobLoaders — register every new job here\n├── send-jobs.ts             # envelope + QStash request builder\n└── handlers/\n    └── {name}-job.ts        # one file per job\n```\n\n## 1. Create the handler\n\nAdd `apps/web/lib/jobs/handlers/{name}-job.ts`. The `name` must be kebab-case and end in `-job` (enforced by `jobNameSchema`: `/^[a-z][a-z0-9]*(-[a-z0-9]+)*-job$/`).\n\n```ts\nimport * as z from \"zod/v4\";\nimport { defineJob } from \"../index\";\n\nconst inputSchema = z.object({\n  programId: z.string(),\n  partnerId: z.string(),\n});\n\nexport const unbanPartnerJob = defineJob({\n  name: \"unban-partner-job\",\n  schema: inputSchema,\n  defaults: {\n    retries: 3, // optional; QStash retries on 5xx\n    // queue: \"unban-partner\", // optional; named QStash queue\n    // flowControl: { key: \"unban-partner\", parallelism: 20 },\n  },\n  async handle(input) {\n    // skip (permanent / not found) → return (process route returns 2xx, QStash does not retry)\n    // transient failure → throw (process route returns 500, QStash retries)\n  },\n});\n```\n\nExport the const as camelCase `{name}Job` matching the kebab `name`.\n\nReference handlers:\n\n- Simple skip/work: `unban-partner-job.ts`, `create-tremendous-campaign-job.ts`\n- Self-pagination: `folder-deleted-job.ts`, `domain-deleted-job.ts`, `partner-search-sync-job.ts`\n- `defaults.flowControl`: `partner-search-sync-job.ts`\n\n## 2. Register it\n\nAdd a **static** `import()` in [`apps/web/lib/jobs/registry.ts`](apps/web/lib/jobs/registry.ts) `jobLoaders`. The object key must equal `defineJob({ name })`. Webpack code-splits each handler.\n\n```ts\n\"unban-partner-job\": () =>\n  import(\"./handlers/unban-partner-job\").then((m) => m.unbanPartnerJob),\n```\n\nDo not register by editing the process route. `loadJob` throws if `job.name !==` the registry key.\n\n## 3. Dispatch from call sites\n\nImport the handler (not `qstash`) and call `dispatch` / `dispatchBatch`:\n\n```ts\nimport { unbanPartnerJob } from \"@/lib/jobs/handlers/unban-partner-job\";\n\nawait unbanPartnerJob.dispatch(\n  { workspaceId, programId, partnerId },\n  { label: partnerId },\n);\n\nawait folderDeletedJob.dispatchBatch(\n  folderIds.map((folderId) => ({ folderId })),\n  ({ folderId }) => ({ label: folderId }),\n);\n```\n\nPer-dispatch options merge over `defaults`: `delay`, `notBefore`, `deduplicationId`, `retries`, `queue`, `flowControl`, `label`.\n\nFailed QStash publish is persisted to the jobs outbox automatically — do not catch-and-swallow unless even the outbox persist failing must not fail the source mutation (see `queue-partner-search-sync.ts`).\n\nSelf-pagination: call `theJob.dispatch(nextPayload, { delay: 1 })` from `handle` (see `folder-deleted-job`, `partner-search-sync-job`).\n\nDo not call `job.execute` from app code. That is only for the process route (and cron drain shims below).\n\n## 4. Convert an existing cron worker\n\n1. Move the `withCron` body into `handle`. Replace `logAndRespond(\"skip…\")` with `console.info` / `console.error` + `return`.\n2. Register + switch every `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` targeting that cron URL to `job.dispatch` / `dispatchBatch`.\n3. Keep a thin POST shim at the old `/api/cron/...` URL that parses the **old** body (not the job envelope) and calls `job.execute(payload)`. That drains in-flight QStash messages. Do not add a shim for brand-new jobs.\n4. Delete cron-only helpers that moved with the handler.\n\n## Handle semantics\n\n| Outcome | What to do | HTTP from process route | QStash |\n| --- | --- | --- | --- |\n| Work done | `return` | 200 | stop |\n| Skip (not found, already done, env not configured) | `console.*` + `return` | 200 | stop |\n| Bad payload | throw `ZodError` (schema.parse) | 200 | stop (non-retryable) |\n| Transient failure | `throw` | 500 | retry |\n\nUnknown job names and invalid envelopes also return 2xx so QStash does not retry forever.\n\n## Do not use defineJob for\n\n- Vercel GET crons in `apps/web/vercel.json`\n- Scanners that only fan out work (the worker they enqueue can be a job)\n- Importers that republish continuation state to the same URL\n- `/api/cron/queue/retry` (job replay infrastructure)\n- Path-param identity (`/api/cron/links/[linkId]/…`) unless the id moves into the payload\n- Outbound webhook forwarding and postbacks\n\n## Do not\n\n- Add a new `/api/jobs/...` route or a new `/api/cron/...` POST worker for payload-driven work.\n- Call `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` with `/api/jobs/process/...` — use `dispatch`.\n- Register jobs with a dynamic `import()` that webpack cannot statically analyze, or a key that differs from `defineJob({ name })`.\n- Name a job without the `-job` suffix.\n- Use `job.execute` at dispatch call sites.\n- Run `pnpm build` after adding a job.\n",".agents/skills/playwright-api-tests/SKILL.md":"---\nname: playwright-api-tests\ndescription: Add or extend Playwright HTTP API specs under apps/web/playwright/api using the shared api/workspace fixtures and Bearer auth from globalSetup. Use when writing Playwright API tests, e2e API specs, or testing /api routes with the api project (not Vitest under apps/web/tests, not browser UI e2e).\n---\n\n# Playwright API tests\n\nHTTP API coverage lives in `apps/web/playwright/api/<resource>/*.spec.ts`. Auth is seeded once by Playwright `globalSetup` (`apps/web/global-setup.ts` → `setup-test-workspace.ts`) into `playwright/.auth/api.json`. Specs use the custom `test` from `../fixtures` — not `@playwright/test` directly for `test`.\n\nDo **not** put these under `apps/web/tests/` (Vitest) or under `playwright/partners` / `playwright/workspaces` (browser e2e).\n\n## Before writing\n\n1. Find the route handler and response shape (Zod schemas under `apps/web/lib/zod`, types under `@/lib/types` or Prisma).\n2. Prefer mirroring an existing nearby resource folder: `tags/`, `folders/`, `customers/`, `workspaces/`.\n3. Confirm the endpoint is workspace-scoped Bearer API (most `/api/*` routes). The fixture already sends `Authorization: Bearer …`.\n\n## What to cover\n\nBefore considering an endpoint covered, ask:\n\n- What happens with valid input?\n- What happens with invalid input?\n- Who is allowed to call it?\n- Can another workspace access the resource?\n- What happens when the resource does not exist?\n- What happens with duplicate data?\n- What happens at boundary values?\n- What state does the operation leave behind?\n- Can the operation be safely retried?\n- Does pagination/filtering/sorting behave correctly?\n- What happens when multiple requests happen concurrently?\n- Are errors returned in the documented format?\n\nNot every endpoint needs a test for every item. The goal is to cover the actual failure modes and API contract, not to maximize the number of tests.\n\n## File layout\n\n```\napps/web/playwright/api/\n├── fixtures.ts              # api + workspace fixtures (reuse; don't duplicate)\n├── setup-test-workspace.ts  # globalSetup seed only — extend only if auth/workspace setup must change\n└── <resource>/\n    ├── <resource>.spec.ts\n    └── <resource>-pagination.spec.ts  # optional split for heavy pagination suites\n```\n\nOne folder per resource. Name files `kebab-case.spec.ts` (`tags/tags.spec.ts`, `customers/customers-pagination.spec.ts`).\n\n## Spec template\n\n```ts\nimport { expect } from \"@playwright/test\";\nimport { randomName } from \"../../utils\";\nimport { test, type ApiClient } from \"../fixtures\";\n\nasync function createThing(\n  api: ApiClient,\n  overrides: Record<string, unknown> = {},\n) {\n  return api.post<YourType>(\"/api/things\", {\n    name: randomName(\"thing\"),\n    ...overrides,\n  });\n}\n\nasync function deleteThing(api: ApiClient, id: string | undefined) {\n  if (!id) return;\n  await api.delete(`/api/things/${id}`);\n}\n\ntest(\"POST /things\", async ({ api }) => {\n  let id: string | undefined;\n\n  try {\n    const body = { name: randomName(\"thing\") };\n    const { status, data } = await api.post<YourType>(\"/api/things\", body);\n    id = data.id;\n\n    expect(status).toEqual(201);\n    expect(data).toStrictEqual({\n      id: expect.any(String),\n      ...body,\n      // stable nulls / defaults from the API\n    });\n  } finally {\n    await deleteThing(api, id);\n  }\n});\n```\n\n## Required conventions\n\n| Rule                                            | Detail                                                                                                        |\n| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| Import `test` from `../fixtures`                | Provides `api`, `workspace`, and `program`                                                                    |\n| Serial only when tests share state              | API project is `fullyParallel: true`. Do not add `mode: \"parallel\"`. Use `test.describe.configure({ mode: \"serial\" })` only when tests in a file/describe share state (e.g. domains, seeded pagination) |\n| Cleanup in `finally`                            | Create → assert → always delete created rows                                                                  |\n| Unique names/ids                                | Use `randomName` / `randomCustomer` / `randomPartnerEmail` from `../../utils` — never fixed colliding names   |\n| Assert status + body                            | Prefer `toStrictEqual` / `toEqual` on full shapes; use `expect.any(String)` for ids/timestamps                |\n| Default shape once                              | Happy-path POST owns the full default resource (and nested) shape. Variant tests assert only what they change |\n| HTTP contract only                              | Assert status + JSON. Do not poll/sleep for `waitUntil`, R2, or other background jobs — CI has no `STORAGE_*` |\n| Error responses                                 | Match `{ error: { code, message, doc_url } }` exactly                                                         |\n| Typed generics                                  | `api.get<T>`, `api.post<T>`, etc.                                                                             |\n| Seeded fixtures                                 | `{ workspace }`, `{ program }` (`id`, `defaultGroupId`), `TEST_WORKSPACE` — not Vitest `E2E_*` constants      |\n\n### Fixtures (`api` / `workspace` / `program`)\n\n- `api.get/post/patch/delete(url, data?)` → `{ status, data }` (JSON parsed).\n- Paths are app-relative (`/api/...`); `baseURL` is `http://localhost:8888`.\n- `workspace.id` / `workspace.slug` and `program.id` / `program.defaultGroupId` come from `.auth/api.json`.\n- Seeded program domain/url: `TEST_WORKSPACE.program` in `setup-test-workspace.ts`.\n\n### Helpers\n\n- Shared randomizers / sort assertions: `apps/web/playwright/utils.ts`.\n- Local `createX` / `deleteX` helpers in the spec file when the same setup repeats.\n- Once `createX` exists, use it for setup **and** follow-up creates (pass overrides for identity fields that must stay stable, e.g. `email`). Happy-path tests may still inline `api.post` so the asserted body is local.\n- Prisma direct seed is allowed for bulk fixtures (see `customers/customers-pagination.spec.ts`); still clean up in `finally` / after hooks. No DELETE route → Prisma/`conn` cleanup is fine (`partners/partners.spec.ts`).\n\n### Error / table-driven cases\n\n```ts\nconst errorCases = [\n  {\n    name: \"POST /things – missing name\",\n    body: {},\n    expected: {\n      status: 422,\n      data: {\n        error: {\n          code: \"unprocessable_entity\",\n          message: \"…\",\n          doc_url:\n            \"https://dub.co/docs/api-reference/errors#unprocessable-entity\",\n        },\n      },\n    },\n  },\n];\n\nfor (const { name, body, expected } of errorCases) {\n  test(name, async ({ api }) => {\n    expect(await api.post(\"/api/things\", body)).toEqual(expected);\n  });\n}\n```\n\nOptional: validate richer payloads with Zod (`.parse(...)`) like `workspaces/workspaces.spec.ts` / enriched customer fields.\n\n## Migrating Vitest API tests\n\nWhen converting `apps/web/tests/<resource>/*.test.ts`:\n\n1. Add or extend `playwright/api/<resource>/<resource>.spec.ts` — do not leave a parallel Vitest HTTP spec.\n2. Map `IntegrationHarness` / `http.post({ path })` → `api` fixture (`/api/...` paths).\n3. Replace `E2E_*` / `E2E_PARTNER_GROUP` with `{ workspace }`, `{ program }`, and `TEST_WORKSPACE`.\n4. Delete the Vitest file after the Playwright spec covers its cases.\n\n## Do not\n\n- Call `setupTestWorkspace` from specs (only `globalSetup`).\n- Import browser `page` / storage-state auth for API specs.\n- Commit secrets or change the fixed Playwright token unless intentionally rotating local/CI test auth.\n- Skip cleanup (parallel API project will leak / flake).\n- Run `pnpm build` after adding tests.\n- Poll or `setTimeout` for R2/storage/`waitUntil` side effects (no `STORAGE_*` in CI). Assert the immediate JSON body instead (e.g. foreign `image` URLs stay `null` on create).\n- Copy Vitest `IntegrationHarness` or `E2E_*` constants into Playwright specs.\n\n## Run\n\nDev server on port 8888 (local), then:\n\n```sh\npnpm --filter web test:e2e --project=api\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts -g \"POST /tags\"\n```\n\n`-g` / `--grep` matches the test title (regex). Combine with a file path to narrow further.\nIn CI, `playwright.config.ts` starts `pnpm start -p 8888` via `webServer`.\n\nMore context: `apps/web/playwright/README.md` (API section).\n","packages/cli/README.md":"# `dub-cli`\n\nA CLI for easily shortening URLs with the [Dub API](https://dub.co/api).\n\nhttps://github.com/user-attachments/assets/2ce9fe51-68ab-4e6d-b08d-4da09c17f90e\n\n## Available Commands\n\n| Command                   | Description                                                                                                                                                                                              |\n| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `dub login`               | Log into the DUB platform                                                                                                                                                                                |\n| `dub config`              | See your configured workspace credentials                                                                                                                                                                |\n| `dub domains`             | Configure your workspace domain                                                                                                                                                                          |\n| `dub shorten [url] [key]` | Create a short link. You can preemptively pass the URL and the generated short link key, or go through the CLI prompts.                                                                                  |\n| `dub links [options]`     | Search for links in your Dub workspace. Available options include: `-s, --search <search>` to search for a link by name, or `-l, --limit <limit>` to limit the number of links returned (default is 10). |\n| `dub help [command]`      | Display help for a specific command                                                                                                                                                                      |\n\n## Running Locally for Development\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package in watch mode:\n   ```bash\n   pnpm dev\n   ```\n3. In a separate terminal, navigate to the `cli` folder again and run an [available command](#available-commands):\n   ```bash\n   pnpm start [command]\n   ```\n4. See all [available commands](#available-commands) and options:\n   ```bash\n   pnpm start help\n   ```\n\n## Testing Production-like Setup\n\n> **Warning**\n> If you have previously installed `dub-cli` globally, uninstall it first to avoid conflicts\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package:\n   ```bash\n   pnpm build\n   ```\n3. Link the package globally:\n   ```bash\n    npm link\n   ```\n4. Verify the installation:\n   ```bash\n    dub -v\n   ```\n5. Run commands:\n   ```bash\n   dub [command]\n   ```\n","packages/embeds/core/README.md":"# `@dub/embed-core`\n\n`@dub/embed-core` is a library that contains the core logic for embedding Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-core\n```\n\n## License\n\n[MIT](./LICENSE.md)\n","packages/embeds/react/README.md":"# `@dub/embed-react`\n\n`@dub/embed-react` is a library of React components that are used embed Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-react\n```\n\n## License\n\n[MIT](./LICENSE.md)\n","packages/hubspot-app/CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working on HubSpot components\n\nIMPORTANT: IF THE 'HubSpot' MCP SERVER IS INSTALLED USE THE TOOLS BEFORE TRYING TO MANUALLY USE CLI COMMANDS OR BEFORE TRYING TO DO ANYTHING WITH HUBSPOT ASSETS\n\n## HubSpot Project Information\n\n- The project configuration is in the `hsproject.json` file\n- A directory is considered a part of the project it or a directory above it contains a `hsproject.json` file\n- The project src directory is defined in the `srcDir` field in the `hsproject.json`\n- The project's platform version is defined in `platformVersion` in the `hs project.json`\n- The `platformVersion` determines what features the project has access to as well as the shape of the configuration files\n\n## npm packages\n\n### `@hubspot/ui-extensions`\n\n- In the `@hubspot/ui-extensions` npm package, only the component properties defined by the component are valid. `style` properties are not valid\n\n## Component Information\n\n### General\n\n- Component configuration files must end with `-hsmeta.json`\n- The `uid` field in the `-hsmeta.json` files must be unique with the project\n- The `type` field in the `-hsmeta.json` files defines the type of the component\n- Components can not be in nested subdirectories, only the specified directories in their corresponding component rules.\n- Example components can be found in https://github.com/HubSpot/hubspot-project-components. The directories are split up by platform version and follow this format `${platformVersion}/components`\n- All component subdirectories must be in the project source directory\n\n### app component\n\n- There can only be one `app` component\n- `app` component must be in the `app` directory\n- If the `config.distribution` field is set to `marketplace`, the only valid `config.auth.type` value is `oauth`\n\n### card\n\n- `card` components must be in the `app/cards` directory\n- The global `window` object is not available in the `card` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `card` components\n\n### app-event\n\n- `app-event` components must be in the `app/app-events` directory\n-\n\n### app-object\n\n- `app-object` components must be in the `app/app-object` directory\n\n### app-function\n\n- `app-function` components must be in the `app/functions` directory\n- `app-function` components are not available when `config.distribution` is set to `marketplace` in the `app` component `-hsmeta.son` file\n\n# settings\n\n- There can only be one `settings` component\n- `settings` components must be in the `app/settings` directory\n- The global `window` object is not available in the `settings` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `settings` components\n- React Components from `@hubspot/ui-extensions/crm` cannot be used in `settings` components\n\n# scim\n\n- There can only be one `scim` component\n- `scim` components must be in the `app/scim` directory\n\n# webhooks\n\n- There can only be one `webhooks` component.\n- `webhooks` components must be in the `app/webhooks` directory\n- `webhooks` components can only be in projects where `config.distribution` is private and `config.auth.type` is `static`\n\n### workflow-actions\n\n- `workflow-action` components must be in the `app/workflow-actions` directory\n\n## HubSpot CLI commands\n\n- All the commands and subcommands have a `--help` argument that provides details on the command and it's arguments\n- The help output is standard yargs output\n- The commands for working with projects in HubSpot are subcommands of `hs project`\n- Debugging flag that can be added to `hs` commands and subcommands: `--debug`\n- Debugging problems with CLI installation: `hs doctor`\n- `hs project open` will open the current project page in the browser\n- `hs init` is required to set up the hubspot configuration file\n- `hs auth` will authenticate a new account. This will require a user to open a browser and paste a token in a CLI prompt.\n- All the commands for managing HubSpot accounts in the CLI are subcommands of `hs account`\n\n## General\n\n- Follow existing patterns in the codebase\n- Use proper component structure based on component `type` in the `-hsmeta.json` file\n- Ensure configuration files follow HubSpot naming conventions\n- Always validate that components are placed in correct directories\n","packages/hubspot-app/README.md":"# Dub HubSpot App\n\nThis package contains the HubSpot app integration for Dub.\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Upload the project to HubSpot account\npnpm hs project upload --account=<account-id>\n\n# Set up a test environment and start local development\npnpm hs project dev\n\n# Install all project dependencies\npnpm hs project install-deps\n\n# Add new feature to app\npnpm hs project add\n\n# List all accounts connected\npnpm hs account list\n```\n","packages/stripe-app/README.md":"# Stripe App\n\nThis is the [Stripe app](https://marketplace.stripe.com/apps/dub-conversions) for Dub Conversions.\n\n## Publish new version\n\n1. Run `stripe login` in your terminal and sign in to `Dub Technologies, Inc.`.\n2. Navigate to the `packages/stripe-app` directory.\n3. Increment the `version` field in `stripe-app.json`.\n4. Upload the updated app using `stripe apps upload`.\n5. Publish the new version via the [Stripe dashboard](https://dashboard.stripe.com/apps/dub.co).\n\n## Run locally\n\n```\nstripe apps start\n```\n","packages/tinybird/README.md":"# Dub Tinybird Setup\n\n```bash\n├── datasources\n│   └── dub_click_events.datasource\n│   └── dub_click_events_mv.datasource\n│   └── dub_links_metadata.datasource\n│   └── dub_links_metadata_latest.datasource\n├── endpoints\n│   ├── browser.pipe\n│   ├── city.pipe\n│   ├── clicks.pipe\n│   ├── country.pipe\n│   ├── device.pipe\n│   ├── os.pipe\n│   └── referer.pipe\n│   └── timeseries.pipe\n│   └── top_links.pipe\n│   └── top_urls.pipe\n├── pipes\n│   ├── dub_click_events_pipe.pipe\n│   ├── dub_links_metadata_pipe.pipe\n```\n","packages/ui/README.md":"# `@dub/ui`\n\n`@dub/ui` is a library of React components that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/ui\n```\n","packages/ui/src/icons/nucleo/README.md":"# Nucleo Icons\n\n> _❗ These icons are copyrighted and redistribution is prohibited. See the full Nucleo copyright notice below._\n\nCopyright © Nucleo\n\nVersion 1.3, January 3, 2024\n\nNucleo Icons\n\nhttps://nucleoapp.com/\n\n- Redistribution of icons is prohibited.\n- Icons are restricted for use only within the product they are bundled with.\n\nFor more details:\n\nhttps://nucleoapp.com/license\n","packages/utils/README.md":"# `@dub/utils`\n\n`@dub/utils` is a library of utility functions that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/utils\n```\n","packages/workers/r2-object-created/README.md":"# r2-object-created Worker (thin forwarder)\n\nCloudflare Queue **push** consumer that forwards each R2 `object-create` notification to Next.js. Magic-byte validation, delete-on-mismatch, and Axiom logging run in the app.\n\n## Quick Edit\n\n1. Paste [`worker.js`](./worker.js) into Workers Quick Edit → Deploy.\n2. Settings → Bindings / Variables / Secrets (below).\n3. Confirm public R2 bucket **Event notification** `object-create` → matching queue.\n\n## Bindings\n\n| Type              | Name                               | Value                                                     |\n| ----------------- | ---------------------------------- | --------------------------------------------------------- |\n| Queues (consumer) | —                                  | `r2-object-created` or `-dev`                             |\n| Variable          | `DUB_WEBHOOK_URL`                  | `https://<host>/api/cloudflare/webhook/r2-object-created` |\n| Secret            | `CLOUDFLARE_WORKER_WEBHOOK_SECRET` | Same as Next.js `CLOUDFLARE_WORKER_WEBHOOK_SECRET`        |\n\nNo R2 or Axiom bindings on the Worker — Next.js uses `STORAGE_*` and Axiom already.\n\n## Flow\n\n```text\nR2 object-create → Queue → Worker → POST /api/cloudflare/webhook/r2-object-created\n  → magic-byte check → delete public object on mismatch → Axiom\n```\n"},"items":[{"name":"SKILL.md","path":".agents/skills/jobs-define-job/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/.agents/skills/jobs-define-job/SKILL.md","title":"jobs-define-job","category":"anthropic-skill","format":"markdown","content":"---\nname: jobs-define-job\ndescription: Add or migrate background jobs with defineJob, register them in jobLoaders, and dispatch via job.dispatch/dispatchBatch instead of qstash.publishJSON to a cron URL. Use when adding a QStash worker, converting a POST /api/cron route into a job, or when the user mentions defineJob, job handlers, /api/jobs/process, or background jobs.\n---\n\n# Background jobs: use defineJob\n\nPayload-driven QStash work goes through `defineJob`. Do **not** add a new HTTP route under `/api/jobs` — every job is executed by the existing [`apps/web/app/api/jobs/process/[jobName]/route.ts`](apps/web/app/api/jobs/process/[jobName]/route.ts).\n\nKeep using `withCron` for Vercel GET schedules and for cron scanners that have no job envelope. See the `cron-use-with-cron` skill for those.\n\n## File layout\n\n```\napps/web/lib/jobs/\n├── index.ts                 # defineJob — do not edit unless changing the framework\n├── registry.ts              # jobLoaders — register every new job here\n├── send-jobs.ts             # envelope + QStash request builder\n└── handlers/\n    └── {name}-job.ts        # one file per job\n```\n\n## 1. Create the handler\n\nAdd `apps/web/lib/jobs/handlers/{name}-job.ts`. The `name` must be kebab-case and end in `-job` (enforced by `jobNameSchema`: `/^[a-z][a-z0-9]*(-[a-z0-9]+)*-job$/`).\n\n```ts\nimport * as z from \"zod/v4\";\nimport { defineJob } from \"../index\";\n\nconst inputSchema = z.object({\n  programId: z.string(),\n  partnerId: z.string(),\n});\n\nexport const unbanPartnerJob = defineJob({\n  name: \"unban-partner-job\",\n  schema: inputSchema,\n  defaults: {\n    retries: 3, // optional; QStash retries on 5xx\n    // queue: \"unban-partner\", // optional; named QStash queue\n    // flowControl: { key: \"unban-partner\", parallelism: 20 },\n  },\n  async handle(input) {\n    // skip (permanent / not found) → return (process route returns 2xx, QStash does not retry)\n    // transient failure → throw (process route returns 500, QStash retries)\n  },\n});\n```\n\nExport the const as camelCase `{name}Job` matching the kebab `name`.\n\nReference handlers:\n\n- Simple skip/work: `unban-partner-job.ts`, `create-tremendous-campaign-job.ts`\n- Self-pagination: `folder-deleted-job.ts`, `domain-deleted-job.ts`, `partner-search-sync-job.ts`\n- `defaults.flowControl`: `partner-search-sync-job.ts`\n\n## 2. Register it\n\nAdd a **static** `import()` in [`apps/web/lib/jobs/registry.ts`](apps/web/lib/jobs/registry.ts) `jobLoaders`. The object key must equal `defineJob({ name })`. Webpack code-splits each handler.\n\n```ts\n\"unban-partner-job\": () =>\n  import(\"./handlers/unban-partner-job\").then((m) => m.unbanPartnerJob),\n```\n\nDo not register by editing the process route. `loadJob` throws if `job.name !==` the registry key.\n\n## 3. Dispatch from call sites\n\nImport the handler (not `qstash`) and call `dispatch` / `dispatchBatch`:\n\n```ts\nimport { unbanPartnerJob } from \"@/lib/jobs/handlers/unban-partner-job\";\n\nawait unbanPartnerJob.dispatch(\n  { workspaceId, programId, partnerId },\n  { label: partnerId },\n);\n\nawait folderDeletedJob.dispatchBatch(\n  folderIds.map((folderId) => ({ folderId })),\n  ({ folderId }) => ({ label: folderId }),\n);\n```\n\nPer-dispatch options merge over `defaults`: `delay`, `notBefore`, `deduplicationId`, `retries`, `queue`, `flowControl`, `label`.\n\nFailed QStash publish is persisted to the jobs outbox automatically — do not catch-and-swallow unless even the outbox persist failing must not fail the source mutation (see `queue-partner-search-sync.ts`).\n\nSelf-pagination: call `theJob.dispatch(nextPayload, { delay: 1 })` from `handle` (see `folder-deleted-job`, `partner-search-sync-job`).\n\nDo not call `job.execute` from app code. That is only for the process route (and cron drain shims below).\n\n## 4. Convert an existing cron worker\n\n1. Move the `withCron` body into `handle`. Replace `logAndRespond(\"skip…\")` with `console.info` / `console.error` + `return`.\n2. Register + switch every `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` targeting that cron URL to `job.dispatch` / `dispatchBatch`.\n3. Keep a thin POST shim at the old `/api/cron/...` URL that parses the **old** body (not the job envelope) and calls `job.execute(payload)`. That drains in-flight QStash messages. Do not add a shim for brand-new jobs.\n4. Delete cron-only helpers that moved with the handler.\n\n## Handle semantics\n\n| Outcome | What to do | HTTP from process route | QStash |\n| --- | --- | --- | --- |\n| Work done | `return` | 200 | stop |\n| Skip (not found, already done, env not configured) | `console.*` + `return` | 200 | stop |\n| Bad payload | throw `ZodError` (schema.parse) | 200 | stop (non-retryable) |\n| Transient failure | `throw` | 500 | retry |\n\nUnknown job names and invalid envelopes also return 2xx so QStash does not retry forever.\n\n## Do not use defineJob for\n\n- Vercel GET crons in `apps/web/vercel.json`\n- Scanners that only fan out work (the worker they enqueue can be a job)\n- Importers that republish continuation state to the same URL\n- `/api/cron/queue/retry` (job replay infrastructure)\n- Path-param identity (`/api/cron/links/[linkId]/…`) unless the id moves into the payload\n- Outbound webhook forwarding and postbacks\n\n## Do not\n\n- Add a new `/api/jobs/...` route or a new `/api/cron/...` POST worker for payload-driven work.\n- Call `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` with `/api/jobs/process/...` — use `dispatch`.\n- Register jobs with a dynamic `import()` that webpack cannot statically analyze, or a key that differs from `defineJob({ name })`.\n- Name a job without the `-job` suffix.\n- Use `job.execute` at dispatch call sites.\n- Run `pnpm build` after adding a job.\n","frontmatter":{"name":"jobs-define-job","description":"Add or migrate background jobs with defineJob, register them in jobLoaders, and dispatch via job.dispatch/dispatchBatch instead of qstash.publishJSON to a cron URL. Use when adding a QStash worker, converting a POST /api/cron route into a job, or when the user mentions defineJob, job handlers, /api/jobs/process, or background jobs."},"isInternal":false,"tokens":1521,"sizeBytes":5702},{"name":"SKILL.md","path":".agents/skills/playwright-api-tests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/.agents/skills/playwright-api-tests/SKILL.md","title":"playwright-api-tests","category":"anthropic-skill","format":"markdown","content":"---\nname: playwright-api-tests\ndescription: Add or extend Playwright HTTP API specs under apps/web/playwright/api using the shared api/workspace fixtures and Bearer auth from globalSetup. Use when writing Playwright API tests, e2e API specs, or testing /api routes with the api project (not Vitest under apps/web/tests, not browser UI e2e).\n---\n\n# Playwright API tests\n\nHTTP API coverage lives in `apps/web/playwright/api/<resource>/*.spec.ts`. Auth is seeded once by Playwright `globalSetup` (`apps/web/global-setup.ts` → `setup-test-workspace.ts`) into `playwright/.auth/api.json`. Specs use the custom `test` from `../fixtures` — not `@playwright/test` directly for `test`.\n\nDo **not** put these under `apps/web/tests/` (Vitest) or under `playwright/partners` / `playwright/workspaces` (browser e2e).\n\n## Before writing\n\n1. Find the route handler and response shape (Zod schemas under `apps/web/lib/zod`, types under `@/lib/types` or Prisma).\n2. Prefer mirroring an existing nearby resource folder: `tags/`, `folders/`, `customers/`, `workspaces/`.\n3. Confirm the endpoint is workspace-scoped Bearer API (most `/api/*` routes). The fixture already sends `Authorization: Bearer …`.\n\n## What to cover\n\nBefore considering an endpoint covered, ask:\n\n- What happens with valid input?\n- What happens with invalid input?\n- Who is allowed to call it?\n- Can another workspace access the resource?\n- What happens when the resource does not exist?\n- What happens with duplicate data?\n- What happens at boundary values?\n- What state does the operation leave behind?\n- Can the operation be safely retried?\n- Does pagination/filtering/sorting behave correctly?\n- What happens when multiple requests happen concurrently?\n- Are errors returned in the documented format?\n\nNot every endpoint needs a test for every item. The goal is to cover the actual failure modes and API contract, not to maximize the number of tests.\n\n## File layout\n\n```\napps/web/playwright/api/\n├── fixtures.ts              # api + workspace fixtures (reuse; don't duplicate)\n├── setup-test-workspace.ts  # globalSetup seed only — extend only if auth/workspace setup must change\n└── <resource>/\n    ├── <resource>.spec.ts\n    └── <resource>-pagination.spec.ts  # optional split for heavy pagination suites\n```\n\nOne folder per resource. Name files `kebab-case.spec.ts` (`tags/tags.spec.ts`, `customers/customers-pagination.spec.ts`).\n\n## Spec template\n\n```ts\nimport { expect } from \"@playwright/test\";\nimport { randomName } from \"../../utils\";\nimport { test, type ApiClient } from \"../fixtures\";\n\nasync function createThing(\n  api: ApiClient,\n  overrides: Record<string, unknown> = {},\n) {\n  return api.post<YourType>(\"/api/things\", {\n    name: randomName(\"thing\"),\n    ...overrides,\n  });\n}\n\nasync function deleteThing(api: ApiClient, id: string | undefined) {\n  if (!id) return;\n  await api.delete(`/api/things/${id}`);\n}\n\ntest(\"POST /things\", async ({ api }) => {\n  let id: string | undefined;\n\n  try {\n    const body = { name: randomName(\"thing\") };\n    const { status, data } = await api.post<YourType>(\"/api/things\", body);\n    id = data.id;\n\n    expect(status).toEqual(201);\n    expect(data).toStrictEqual({\n      id: expect.any(String),\n      ...body,\n      // stable nulls / defaults from the API\n    });\n  } finally {\n    await deleteThing(api, id);\n  }\n});\n```\n\n## Required conventions\n\n| Rule                                            | Detail                                                                                                        |\n| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| Import `test` from `../fixtures`                | Provides `api`, `workspace`, and `program`                                                                    |\n| Serial only when tests share state              | API project is `fullyParallel: true`. Do not add `mode: \"parallel\"`. Use `test.describe.configure({ mode: \"serial\" })` only when tests in a file/describe share state (e.g. domains, seeded pagination) |\n| Cleanup in `finally`                            | Create → assert → always delete created rows                                                                  |\n| Unique names/ids                                | Use `randomName` / `randomCustomer` / `randomPartnerEmail` from `../../utils` — never fixed colliding names   |\n| Assert status + body                            | Prefer `toStrictEqual` / `toEqual` on full shapes; use `expect.any(String)` for ids/timestamps                |\n| Default shape once                              | Happy-path POST owns the full default resource (and nested) shape. Variant tests assert only what they change |\n| HTTP contract only                              | Assert status + JSON. Do not poll/sleep for `waitUntil`, R2, or other background jobs — CI has no `STORAGE_*` |\n| Error responses                                 | Match `{ error: { code, message, doc_url } }` exactly                                                         |\n| Typed generics                                  | `api.get<T>`, `api.post<T>`, etc.                                                                             |\n| Seeded fixtures                                 | `{ workspace }`, `{ program }` (`id`, `defaultGroupId`), `TEST_WORKSPACE` — not Vitest `E2E_*` constants      |\n\n### Fixtures (`api` / `workspace` / `program`)\n\n- `api.get/post/patch/delete(url, data?)` → `{ status, data }` (JSON parsed).\n- Paths are app-relative (`/api/...`); `baseURL` is `http://localhost:8888`.\n- `workspace.id` / `workspace.slug` and `program.id` / `program.defaultGroupId` come from `.auth/api.json`.\n- Seeded program domain/url: `TEST_WORKSPACE.program` in `setup-test-workspace.ts`.\n\n### Helpers\n\n- Shared randomizers / sort assertions: `apps/web/playwright/utils.ts`.\n- Local `createX` / `deleteX` helpers in the spec file when the same setup repeats.\n- Once `createX` exists, use it for setup **and** follow-up creates (pass overrides for identity fields that must stay stable, e.g. `email`). Happy-path tests may still inline `api.post` so the asserted body is local.\n- Prisma direct seed is allowed for bulk fixtures (see `customers/customers-pagination.spec.ts`); still clean up in `finally` / after hooks. No DELETE route → Prisma/`conn` cleanup is fine (`partners/partners.spec.ts`).\n\n### Error / table-driven cases\n\n```ts\nconst errorCases = [\n  {\n    name: \"POST /things – missing name\",\n    body: {},\n    expected: {\n      status: 422,\n      data: {\n        error: {\n          code: \"unprocessable_entity\",\n          message: \"…\",\n          doc_url:\n            \"https://dub.co/docs/api-reference/errors#unprocessable-entity\",\n        },\n      },\n    },\n  },\n];\n\nfor (const { name, body, expected } of errorCases) {\n  test(name, async ({ api }) => {\n    expect(await api.post(\"/api/things\", body)).toEqual(expected);\n  });\n}\n```\n\nOptional: validate richer payloads with Zod (`.parse(...)`) like `workspaces/workspaces.spec.ts` / enriched customer fields.\n\n## Migrating Vitest API tests\n\nWhen converting `apps/web/tests/<resource>/*.test.ts`:\n\n1. Add or extend `playwright/api/<resource>/<resource>.spec.ts` — do not leave a parallel Vitest HTTP spec.\n2. Map `IntegrationHarness` / `http.post({ path })` → `api` fixture (`/api/...` paths).\n3. Replace `E2E_*` / `E2E_PARTNER_GROUP` with `{ workspace }`, `{ program }`, and `TEST_WORKSPACE`.\n4. Delete the Vitest file after the Playwright spec covers its cases.\n\n## Do not\n\n- Call `setupTestWorkspace` from specs (only `globalSetup`).\n- Import browser `page` / storage-state auth for API specs.\n- Commit secrets or change the fixed Playwright token unless intentionally rotating local/CI test auth.\n- Skip cleanup (parallel API project will leak / flake).\n- Run `pnpm build` after adding tests.\n- Poll or `setTimeout` for R2/storage/`waitUntil` side effects (no `STORAGE_*` in CI). Assert the immediate JSON body instead (e.g. foreign `image` URLs stay `null` on create).\n- Copy Vitest `IntegrationHarness` or `E2E_*` constants into Playwright specs.\n\n## Run\n\nDev server on port 8888 (local), then:\n\n```sh\npnpm --filter web test:e2e --project=api\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts -g \"POST /tags\"\n```\n\n`-g` / `--grep` matches the test title (regex). Combine with a file path to narrow further.\nIn CI, `playwright.config.ts` starts `pnpm start -p 8888` via `webServer`.\n\nMore context: `apps/web/playwright/README.md` (API section).\n","frontmatter":{"name":"playwright-api-tests","description":"Add or extend Playwright HTTP API specs under apps/web/playwright/api using the shared api/workspace fixtures and Bearer auth from globalSetup. Use when writing Playwright API tests, e2e API specs, or testing /api routes with the api project (not Vitest under apps/web/tests, not browser UI e2e)."},"isInternal":false,"tokens":1988,"sizeBytes":8687},{"name":"CLAUDE.md","path":"packages/hubspot-app/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/hubspot-app/CLAUDE.md","title":"Hubspot-app Skill","category":"anthropic-skill","format":"markdown","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working on HubSpot components\n\nIMPORTANT: IF THE 'HubSpot' MCP SERVER IS INSTALLED USE THE TOOLS BEFORE TRYING TO MANUALLY USE CLI COMMANDS OR BEFORE TRYING TO DO ANYTHING WITH HUBSPOT ASSETS\n\n## HubSpot Project Information\n\n- The project configuration is in the `hsproject.json` file\n- A directory is considered a part of the project it or a directory above it contains a `hsproject.json` file\n- The project src directory is defined in the `srcDir` field in the `hsproject.json`\n- The project's platform version is defined in `platformVersion` in the `hs project.json`\n- The `platformVersion` determines what features the project has access to as well as the shape of the configuration files\n\n## npm packages\n\n### `@hubspot/ui-extensions`\n\n- In the `@hubspot/ui-extensions` npm package, only the component properties defined by the component are valid. `style` properties are not valid\n\n## Component Information\n\n### General\n\n- Component configuration files must end with `-hsmeta.json`\n- The `uid` field in the `-hsmeta.json` files must be unique with the project\n- The `type` field in the `-hsmeta.json` files defines the type of the component\n- Components can not be in nested subdirectories, only the specified directories in their corresponding component rules.\n- Example components can be found in https://github.com/HubSpot/hubspot-project-components. The directories are split up by platform version and follow this format `${platformVersion}/components`\n- All component subdirectories must be in the project source directory\n\n### app component\n\n- There can only be one `app` component\n- `app` component must be in the `app` directory\n- If the `config.distribution` field is set to `marketplace`, the only valid `config.auth.type` value is `oauth`\n\n### card\n\n- `card` components must be in the `app/cards` directory\n- The global `window` object is not available in the `card` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `card` components\n\n### app-event\n\n- `app-event` components must be in the `app/app-events` directory\n-\n\n### app-object\n\n- `app-object` components must be in the `app/app-object` directory\n\n### app-function\n\n- `app-function` components must be in the `app/functions` directory\n- `app-function` components are not available when `config.distribution` is set to `marketplace` in the `app` component `-hsmeta.son` file\n\n# settings\n\n- There can only be one `settings` component\n- `settings` components must be in the `app/settings` directory\n- The global `window` object is not available in the `settings` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `settings` components\n- React Components from `@hubspot/ui-extensions/crm` cannot be used in `settings` components\n\n# scim\n\n- There can only be one `scim` component\n- `scim` components must be in the `app/scim` directory\n\n# webhooks\n\n- There can only be one `webhooks` component.\n- `webhooks` components must be in the `app/webhooks` directory\n- `webhooks` components can only be in projects where `config.distribution` is private and `config.auth.type` is `static`\n\n### workflow-actions\n\n- `workflow-action` components must be in the `app/workflow-actions` directory\n\n## HubSpot CLI commands\n\n- All the commands and subcommands have a `--help` argument that provides details on the command and it's arguments\n- The help output is standard yargs output\n- The commands for working with projects in HubSpot are subcommands of `hs project`\n- Debugging flag that can be added to `hs` commands and subcommands: `--debug`\n- Debugging problems with CLI installation: `hs doctor`\n- `hs project open` will open the current project page in the browser\n- `hs init` is required to set up the hubspot configuration file\n- `hs auth` will authenticate a new account. This will require a user to open a browser and paste a token in a CLI prompt.\n- All the commands for managing HubSpot accounts in the CLI are subcommands of `hs account`\n\n## General\n\n- Follow existing patterns in the codebase\n- Use proper component structure based on component `type` in the `-hsmeta.json` file\n- Ensure configuration files follow HubSpot naming conventions\n- Always validate that components are placed in correct directories\n","isInternal":false,"tokens":1154,"sizeBytes":4917},{"name":"README.md","path":"packages/cli/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/cli/README.md","title":"cli Documentation","category":"plugin-manifest","format":"markdown","content":"# `dub-cli`\n\nA CLI for easily shortening URLs with the [Dub API](https://dub.co/api).\n\nhttps://github.com/user-attachments/assets/2ce9fe51-68ab-4e6d-b08d-4da09c17f90e\n\n## Available Commands\n\n| Command                   | Description                                                                                                                                                                                              |\n| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `dub login`               | Log into the DUB platform                                                                                                                                                                                |\n| `dub config`              | See your configured workspace credentials                                                                                                                                                                |\n| `dub domains`             | Configure your workspace domain                                                                                                                                                                          |\n| `dub shorten [url] [key]` | Create a short link. You can preemptively pass the URL and the generated short link key, or go through the CLI prompts.                                                                                  |\n| `dub links [options]`     | Search for links in your Dub workspace. Available options include: `-s, --search <search>` to search for a link by name, or `-l, --limit <limit>` to limit the number of links returned (default is 10). |\n| `dub help [command]`      | Display help for a specific command                                                                                                                                                                      |\n\n## Running Locally for Development\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package in watch mode:\n   ```bash\n   pnpm dev\n   ```\n3. In a separate terminal, navigate to the `cli` folder again and run an [available command](#available-commands):\n   ```bash\n   pnpm start [command]\n   ```\n4. See all [available commands](#available-commands) and options:\n   ```bash\n   pnpm start help\n   ```\n\n## Testing Production-like Setup\n\n> **Warning**\n> If you have previously installed `dub-cli` globally, uninstall it first to avoid conflicts\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package:\n   ```bash\n   pnpm build\n   ```\n3. Link the package globally:\n   ```bash\n    npm link\n   ```\n4. Verify the installation:\n   ```bash\n    dub -v\n   ```\n5. Run commands:\n   ```bash\n   dub [command]\n   ```\n","isInternal":false,"tokens":502,"sizeBytes":3019},{"name":"README.md","path":"packages/embeds/core/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/embeds/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"# `@dub/embed-core`\n\n`@dub/embed-core` is a library that contains the core logic for embedding Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-core\n```\n\n## License\n\n[MIT](./LICENSE.md)\n","isInternal":false,"tokens":68,"sizeBytes":261},{"name":"README.md","path":"packages/embeds/react/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/embeds/react/README.md","title":"react Documentation","category":"plugin-manifest","format":"markdown","content":"# `@dub/embed-react`\n\n`@dub/embed-react` is a library of React components that are used embed Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-react\n```\n\n## License\n\n[MIT](./LICENSE.md)\n","isInternal":false,"tokens":68,"sizeBytes":261},{"name":"README.md","path":"packages/hubspot-app/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/hubspot-app/README.md","title":"hubspot-app Documentation","category":"plugin-manifest","format":"markdown","content":"# Dub HubSpot App\n\nThis package contains the HubSpot app integration for Dub.\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Upload the project to HubSpot account\npnpm hs project upload --account=<account-id>\n\n# Set up a test environment and start local development\npnpm hs project dev\n\n# Install all project dependencies\npnpm hs project install-deps\n\n# Add new feature to app\npnpm hs project add\n\n# List all accounts connected\npnpm hs account list\n```\n","isInternal":false,"tokens":109,"sizeBytes":470},{"name":"README.md","path":"packages/stripe-app/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/stripe-app/README.md","title":"stripe-app Documentation","category":"plugin-manifest","format":"markdown","content":"# Stripe App\n\nThis is the [Stripe app](https://marketplace.stripe.com/apps/dub-conversions) for Dub Conversions.\n\n## Publish new version\n\n1. Run `stripe login` in your terminal and sign in to `Dub Technologies, Inc.`.\n2. Navigate to the `packages/stripe-app` directory.\n3. Increment the `version` field in `stripe-app.json`.\n4. Upload the updated app using `stripe apps upload`.\n5. Publish the new version via the [Stripe dashboard](https://dashboard.stripe.com/apps/dub.co).\n\n## Run locally\n\n```\nstripe apps start\n```\n","isInternal":false,"tokens":129,"sizeBytes":519},{"name":"README.md","path":"packages/tinybird/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/tinybird/README.md","title":"tinybird Documentation","category":"plugin-manifest","format":"markdown","content":"# Dub Tinybird Setup\n\n```bash\n├── datasources\n│   └── dub_click_events.datasource\n│   └── dub_click_events_mv.datasource\n│   └── dub_links_metadata.datasource\n│   └── dub_links_metadata_latest.datasource\n├── endpoints\n│   ├── browser.pipe\n│   ├── city.pipe\n│   ├── clicks.pipe\n│   ├── country.pipe\n│   ├── device.pipe\n│   ├── os.pipe\n│   └── referer.pipe\n│   └── timeseries.pipe\n│   └── top_links.pipe\n│   └── top_urls.pipe\n├── pipes\n│   ├── dub_click_events_pipe.pipe\n│   ├── dub_links_metadata_pipe.pipe\n```\n","isInternal":false,"tokens":159,"sizeBytes":656},{"name":"README.md","path":"packages/ui/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/ui/README.md","title":"ui Documentation","category":"plugin-manifest","format":"markdown","content":"# `@dub/ui`\n\n`@dub/ui` is a library of React components that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/ui\n```\n","isInternal":false,"tokens":51,"sizeBytes":176},{"name":"README.md","path":"packages/ui/src/icons/nucleo/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/ui/src/icons/nucleo/README.md","title":"nucleo Documentation","category":"plugin-manifest","format":"markdown","content":"# Nucleo Icons\n\n> _❗ These icons are copyrighted and redistribution is prohibited. See the full Nucleo copyright notice below._\n\nCopyright © Nucleo\n\nVersion 1.3, January 3, 2024\n\nNucleo Icons\n\nhttps://nucleoapp.com/\n\n- Redistribution of icons is prohibited.\n- Icons are restricted for use only within the product they are bundled with.\n\nFor more details:\n\nhttps://nucleoapp.com/license\n","isInternal":false,"tokens":97,"sizeBytes":389},{"name":"README.md","path":"packages/utils/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/utils/README.md","title":"utils Documentation","category":"plugin-manifest","format":"markdown","content":"# `@dub/utils`\n\n`@dub/utils` is a library of utility functions that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/utils\n```\n","isInternal":false,"tokens":51,"sizeBytes":186},{"name":"README.md","path":"packages/workers/r2-object-created/README.md","rawUrl":"https://raw.githubusercontent.com/dubinc/dub/HEAD/packages/workers/r2-object-created/README.md","title":"r2-object-created Documentation","category":"plugin-manifest","format":"markdown","content":"# r2-object-created Worker (thin forwarder)\n\nCloudflare Queue **push** consumer that forwards each R2 `object-create` notification to Next.js. Magic-byte validation, delete-on-mismatch, and Axiom logging run in the app.\n\n## Quick Edit\n\n1. Paste [`worker.js`](./worker.js) into Workers Quick Edit → Deploy.\n2. Settings → Bindings / Variables / Secrets (below).\n3. Confirm public R2 bucket **Event notification** `object-create` → matching queue.\n\n## Bindings\n\n| Type              | Name                               | Value                                                     |\n| ----------------- | ---------------------------------- | --------------------------------------------------------- |\n| Queues (consumer) | —                                  | `r2-object-created` or `-dev`                             |\n| Variable          | `DUB_WEBHOOK_URL`                  | `https://<host>/api/cloudflare/webhook/r2-object-created` |\n| Secret            | `CLOUDFLARE_WORKER_WEBHOOK_SECRET` | Same as Next.js `CLOUDFLARE_WORKER_WEBHOOK_SECRET`        |\n\nNo R2 or Axiom bindings on the Worker — Next.js uses `STORAGE_*` and Axiom already.\n\n## Flow\n\n```text\nR2 object-create → Queue → Worker → POST /api/cloudflare/webhook/r2-object-created\n  → magic-byte check → delete public object on mismatch → Axiom\n```\n","isInternal":false,"tokens":285,"sizeBytes":1330}],"systemPromptSnippet":"<yakaai_skills repo=\"dubinc/dub\">\n<!-- File: .agents/skills/jobs-define-job/SKILL.md (Tokens: ~1521 | Category: anthropic-skill) -->\n---\nname: jobs-define-job\ndescription: Add or migrate background jobs with defineJob, register them in jobLoaders, and dispatch via job.dispatch/dispatchBatch instead of qstash.publishJSON to a cron URL. Use when adding a QStash worker, converting a POST /api/cron route into a job, or when the user mentions defineJob, job handlers, /api/jobs/process, or background jobs.\n---\n\n# Background jobs: use defineJob\n\nPayload-driven QStash work goes through `defineJob`. Do **not** add a new HTTP route under `/api/jobs` — every job is executed by the existing [`apps/web/app/api/jobs/process/[jobName]/route.ts`](apps/web/app/api/jobs/process/[jobName]/route.ts).\n\nKeep using `withCron` for Vercel GET schedules and for cron scanners that have no job envelope. See the `cron-use-with-cron` skill for those.\n\n## File layout\n\n```\napps/web/lib/jobs/\n├── index.ts                 # defineJob — do not edit unless changing the framework\n├── registry.ts              # jobLoaders — register every new job here\n├── send-jobs.ts             # envelope + QStash request builder\n└── handlers/\n    └── {name}-job.ts        # one file per job\n```\n\n## 1. Create the handler\n\nAdd `apps/web/lib/jobs/handlers/{name}-job.ts`. The `name` must be kebab-case and end in `-job` (enforced by `jobNameSchema`: `/^[a-z][a-z0-9]*(-[a-z0-9]+)*-job$/`).\n\n```ts\nimport * as z from \"zod/v4\";\nimport { defineJob } from \"../index\";\n\nconst inputSchema = z.object({\n  programId: z.string(),\n  partnerId: z.string(),\n});\n\nexport const unbanPartnerJob = defineJob({\n  name: \"unban-partner-job\",\n  schema: inputSchema,\n  defaults: {\n    retries: 3, // optional; QStash retries on 5xx\n    // queue: \"unban-partner\", // optional; named QStash queue\n    // flowControl: { key: \"unban-partner\", parallelism: 20 },\n  },\n  async handle(input) {\n    // skip (permanent / not found) → return (process route returns 2xx, QStash does not retry)\n    // transient failure → throw (process route returns 500, QStash retries)\n  },\n});\n```\n\nExport the const as camelCase `{name}Job` matching the kebab `name`.\n\nReference handlers:\n\n- Simple skip/work: `unban-partner-job.ts`, `create-tremendous-campaign-job.ts`\n- Self-pagination: `folder-deleted-job.ts`, `domain-deleted-job.ts`, `partner-search-sync-job.ts`\n- `defaults.flowControl`: `partner-search-sync-job.ts`\n\n## 2. Register it\n\nAdd a **static** `import()` in [`apps/web/lib/jobs/registry.ts`](apps/web/lib/jobs/registry.ts) `jobLoaders`. The object key must equal `defineJob({ name })`. Webpack code-splits each handler.\n\n```ts\n\"unban-partner-job\": () =>\n  import(\"./handlers/unban-partner-job\").then((m) => m.unbanPartnerJob),\n```\n\nDo not register by editing the process route. `loadJob` throws if `job.name !==` the registry key.\n\n## 3. Dispatch from call sites\n\nImport the handler (not `qstash`) and call `dispatch` / `dispatchBatch`:\n\n```ts\nimport { unbanPartnerJob } from \"@/lib/jobs/handlers/unban-partner-job\";\n\nawait unbanPartnerJob.dispatch(\n  { workspaceId, programId, partnerId },\n  { label: partnerId },\n);\n\nawait folderDeletedJob.dispatchBatch(\n  folderIds.map((folderId) => ({ folderId })),\n  ({ folderId }) => ({ label: folderId }),\n);\n```\n\nPer-dispatch options merge over `defaults`: `delay`, `notBefore`, `deduplicationId`, `retries`, `queue`, `flowControl`, `label`.\n\nFailed QStash publish is persisted to the jobs outbox automatically — do not catch-and-swallow unless even the outbox persist failing must not fail the source mutation (see `queue-partner-search-sync.ts`).\n\nSelf-pagination: call `theJob.dispatch(nextPayload, { delay: 1 })` from `handle` (see `folder-deleted-job`, `partner-search-sync-job`).\n\nDo not call `job.execute` from app code. That is only for the process route (and cron drain shims below).\n\n## 4. Convert an existing cron worker\n\n1. Move the `withCron` body into `handle`. Replace `logAndRespond(\"skip…\")` with `console.info` / `console.error` + `return`.\n2. Register + switch every `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` targeting that cron URL to `job.dispatch` / `dispatchBatch`.\n3. Keep a thin POST shim at the old `/api/cron/...` URL that parses the **old** body (not the job envelope) and calls `job.execute(payload)`. That drains in-flight QStash messages. Do not add a shim for brand-new jobs.\n4. Delete cron-only helpers that moved with the handler.\n\n## Handle semantics\n\n| Outcome | What to do | HTTP from process route | QStash |\n| --- | --- | --- | --- |\n| Work done | `return` | 200 | stop |\n| Skip (not found, already done, env not configured) | `console.*` + `return` | 200 | stop |\n| Bad payload | throw `ZodError` (schema.parse) | 200 | stop (non-retryable) |\n| Transient failure | `throw` | 500 | retry |\n\nUnknown job names and invalid envelopes also return 2xx so QStash does not retry forever.\n\n## Do not use defineJob for\n\n- Vercel GET crons in `apps/web/vercel.json`\n- Scanners that only fan out work (the worker they enqueue can be a job)\n- Importers that republish continuation state to the same URL\n- `/api/cron/queue/retry` (job replay infrastructure)\n- Path-param identity (`/api/cron/links/[linkId]/…`) unless the id moves into the payload\n- Outbound webhook forwarding and postbacks\n\n## Do not\n\n- Add a new `/api/jobs/...` route or a new `/api/cron/...` POST worker for payload-driven work.\n- Call `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` with `/api/jobs/process/...` — use `dispatch`.\n- Register jobs with a dynamic `import()` that webpack cannot statically analyze, or a key that differs from `defineJob({ name })`.\n- Name a job without the `-job` suffix.\n- Use `job.execute` at dispatch call sites.\n- Run `pnpm build` after adding a job.\n\n<!-- File: .agents/skills/playwright-api-tests/SKILL.md (Tokens: ~1988 | Category: anthropic-skill) -->\n---\nname: playwright-api-tests\ndescription: Add or extend Playwright HTTP API specs under apps/web/playwright/api using the shared api/workspace fixtures and Bearer auth from globalSetup. Use when writing Playwright API tests, e2e API specs, or testing /api routes with the api project (not Vitest under apps/web/tests, not browser UI e2e).\n---\n\n# Playwright API tests\n\nHTTP API coverage lives in `apps/web/playwright/api/<resource>/*.spec.ts`. Auth is seeded once by Playwright `globalSetup` (`apps/web/global-setup.ts` → `setup-test-workspace.ts`) into `playwright/.auth/api.json`. Specs use the custom `test` from `../fixtures` — not `@playwright/test` directly for `test`.\n\nDo **not** put these under `apps/web/tests/` (Vitest) or under `playwright/partners` / `playwright/workspaces` (browser e2e).\n\n## Before writing\n\n1. Find the route handler and response shape (Zod schemas under `apps/web/lib/zod`, types under `@/lib/types` or Prisma).\n2. Prefer mirroring an existing nearby resource folder: `tags/`, `folders/`, `customers/`, `workspaces/`.\n3. Confirm the endpoint is workspace-scoped Bearer API (most `/api/*` routes). The fixture already sends `Authorization: Bearer …`.\n\n## What to cover\n\nBefore considering an endpoint covered, ask:\n\n- What happens with valid input?\n- What happens with invalid input?\n- Who is allowed to call it?\n- Can another workspace access the resource?\n- What happens when the resource does not exist?\n- What happens with duplicate data?\n- What happens at boundary values?\n- What state does the operation leave behind?\n- Can the operation be safely retried?\n- Does pagination/filtering/sorting behave correctly?\n- What happens when multiple requests happen concurrently?\n- Are errors returned in the documented format?\n\nNot every endpoint needs a test for every item. The goal is to cover the actual failure modes and API contract, not to maximize the number of tests.\n\n## File layout\n\n```\napps/web/playwright/api/\n├── fixtures.ts              # api + workspace fixtures (reuse; don't duplicate)\n├── setup-test-workspace.ts  # globalSetup seed only — extend only if auth/workspace setup must change\n└── <resource>/\n    ├── <resource>.spec.ts\n    └── <resource>-pagination.spec.ts  # optional split for heavy pagination suites\n```\n\nOne folder per resource. Name files `kebab-case.spec.ts` (`tags/tags.spec.ts`, `customers/customers-pagination.spec.ts`).\n\n## Spec template\n\n```ts\nimport { expect } from \"@playwright/test\";\nimport { randomName } from \"../../utils\";\nimport { test, type ApiClient } from \"../fixtures\";\n\nasync function createThing(\n  api: ApiClient,\n  overrides: Record<string, unknown> = {},\n) {\n  return api.post<YourType>(\"/api/things\", {\n    name: randomName(\"thing\"),\n    ...overrides,\n  });\n}\n\nasync function deleteThing(api: ApiClient, id: string | undefined) {\n  if (!id) return;\n  await api.delete(`/api/things/${id}`);\n}\n\ntest(\"POST /things\", async ({ api }) => {\n  let id: string | undefined;\n\n  try {\n    const body = { name: randomName(\"thing\") };\n    const { status, data } = await api.post<YourType>(\"/api/things\", body);\n    id = data.id;\n\n    expect(status).toEqual(201);\n    expect(data).toStrictEqual({\n      id: expect.any(String),\n      ...body,\n      // stable nulls / defaults from the API\n    });\n  } finally {\n    await deleteThing(api, id);\n  }\n});\n```\n\n## Required conventions\n\n| Rule                                            | Detail                                                                                                        |\n| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| Import `test` from `../fixtures`                | Provides `api`, `workspace`, and `program`                                                                    |\n| Serial only when tests share state              | API project is `fullyParallel: true`. Do not add `mode: \"parallel\"`. Use `test.describe.configure({ mode: \"serial\" })` only when tests in a file/describe share state (e.g. domains, seeded pagination) |\n| Cleanup in `finally`                            | Create → assert → always delete created rows                                                                  |\n| Unique names/ids                                | Use `randomName` / `randomCustomer` / `randomPartnerEmail` from `../../utils` — never fixed colliding names   |\n| Assert status + body                            | Prefer `toStrictEqual` / `toEqual` on full shapes; use `expect.any(String)` for ids/timestamps                |\n| Default shape once                              | Happy-path POST owns the full default resource (and nested) shape. Variant tests assert only what they change |\n| HTTP contract only                              | Assert status + JSON. Do not poll/sleep for `waitUntil`, R2, or other background jobs — CI has no `STORAGE_*` |\n| Error responses                                 | Match `{ error: { code, message, doc_url } }` exactly                                                         |\n| Typed generics                                  | `api.get<T>`, `api.post<T>`, etc.                                                                             |\n| Seeded fixtures                                 | `{ workspace }`, `{ program }` (`id`, `defaultGroupId`), `TEST_WORKSPACE` — not Vitest `E2E_*` constants      |\n\n### Fixtures (`api` / `workspace` / `program`)\n\n- `api.get/post/patch/delete(url, data?)` → `{ status, data }` (JSON parsed).\n- Paths are app-relative (`/api/...`); `baseURL` is `http://localhost:8888`.\n- `workspace.id` / `workspace.slug` and `program.id` / `program.defaultGroupId` come from `.auth/api.json`.\n- Seeded program domain/url: `TEST_WORKSPACE.program` in `setup-test-workspace.ts`.\n\n### Helpers\n\n- Shared randomizers / sort assertions: `apps/web/playwright/utils.ts`.\n- Local `createX` / `deleteX` helpers in the spec file when the same setup repeats.\n- Once `createX` exists, use it for setup **and** follow-up creates (pass overrides for identity fields that must stay stable, e.g. `email`). Happy-path tests may still inline `api.post` so the asserted body is local.\n- Prisma direct seed is allowed for bulk fixtures (see `customers/customers-pagination.spec.ts`); still clean up in `finally` / after hooks. No DELETE route → Prisma/`conn` cleanup is fine (`partners/partners.spec.ts`).\n\n### Error / table-driven cases\n\n```ts\nconst errorCases = [\n  {\n    name: \"POST /things – missing name\",\n    body: {},\n    expected: {\n      status: 422,\n      data: {\n        error: {\n          code: \"unprocessable_entity\",\n          message: \"…\",\n          doc_url:\n            \"https://dub.co/docs/api-reference/errors#unprocessable-entity\",\n        },\n      },\n    },\n  },\n];\n\nfor (const { name, body, expected } of errorCases) {\n  test(name, async ({ api }) => {\n    expect(await api.post(\"/api/things\", body)).toEqual(expected);\n  });\n}\n```\n\nOptional: validate richer payloads with Zod (`.parse(...)`) like `workspaces/workspaces.spec.ts` / enriched customer fields.\n\n## Migrating Vitest API tests\n\nWhen converting `apps/web/tests/<resource>/*.test.ts`:\n\n1. Add or extend `playwright/api/<resource>/<resource>.spec.ts` — do not leave a parallel Vitest HTTP spec.\n2. Map `IntegrationHarness` / `http.post({ path })` → `api` fixture (`/api/...` paths).\n3. Replace `E2E_*` / `E2E_PARTNER_GROUP` with `{ workspace }`, `{ program }`, and `TEST_WORKSPACE`.\n4. Delete the Vitest file after the Playwright spec covers its cases.\n\n## Do not\n\n- Call `setupTestWorkspace` from specs (only `globalSetup`).\n- Import browser `page` / storage-state auth for API specs.\n- Commit secrets or change the fixed Playwright token unless intentionally rotating local/CI test auth.\n- Skip cleanup (parallel API project will leak / flake).\n- Run `pnpm build` after adding tests.\n- Poll or `setTimeout` for R2/storage/`waitUntil` side effects (no `STORAGE_*` in CI). Assert the immediate JSON body instead (e.g. foreign `image` URLs stay `null` on create).\n- Copy Vitest `IntegrationHarness` or `E2E_*` constants into Playwright specs.\n\n## Run\n\nDev server on port 8888 (local), then:\n\n```sh\npnpm --filter web test:e2e --project=api\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts\npnpm --filter web test:e2e --project=api playwright/api/tags/tags.spec.ts -g \"POST /tags\"\n```\n\n`-g` / `--grep` matches the test title (regex). Combine with a file path to narrow further.\nIn CI, `playwright.config.ts` starts `pnpm start -p 8888` via `webServer`.\n\nMore context: `apps/web/playwright/README.md` (API section).\n\n<!-- File: packages/hubspot-app/CLAUDE.md (Tokens: ~1154 | Category: anthropic-skill) -->\n# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working on HubSpot components\n\nIMPORTANT: IF THE 'HubSpot' MCP SERVER IS INSTALLED USE THE TOOLS BEFORE TRYING TO MANUALLY USE CLI COMMANDS OR BEFORE TRYING TO DO ANYTHING WITH HUBSPOT ASSETS\n\n## HubSpot Project Information\n\n- The project configuration is in the `hsproject.json` file\n- A directory is considered a part of the project it or a directory above it contains a `hsproject.json` file\n- The project src directory is defined in the `srcDir` field in the `hsproject.json`\n- The project's platform version is defined in `platformVersion` in the `hs project.json`\n- The `platformVersion` determines what features the project has access to as well as the shape of the configuration files\n\n## npm packages\n\n### `@hubspot/ui-extensions`\n\n- In the `@hubspot/ui-extensions` npm package, only the component properties defined by the component are valid. `style` properties are not valid\n\n## Component Information\n\n### General\n\n- Component configuration files must end with `-hsmeta.json`\n- The `uid` field in the `-hsmeta.json` files must be unique with the project\n- The `type` field in the `-hsmeta.json` files defines the type of the component\n- Components can not be in nested subdirectories, only the specified directories in their corresponding component rules.\n- Example components can be found in https://github.com/HubSpot/hubspot-project-components. The directories are split up by platform version and follow this format `${platformVersion}/components`\n- All component subdirectories must be in the project source directory\n\n### app component\n\n- There can only be one `app` component\n- `app` component must be in the `app` directory\n- If the `config.distribution` field is set to `marketplace`, the only valid `config.auth.type` value is `oauth`\n\n### card\n\n- `card` components must be in the `app/cards` directory\n- The global `window` object is not available in the `card` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `card` components\n\n### app-event\n\n- `app-event` components must be in the `app/app-events` directory\n-\n\n### app-object\n\n- `app-object` components must be in the `app/app-object` directory\n\n### app-function\n\n- `app-function` components must be in the `app/functions` directory\n- `app-function` components are not available when `config.distribution` is set to `marketplace` in the `app` component `-hsmeta.son` file\n\n# settings\n\n- There can only be one `settings` component\n- `settings` components must be in the `app/settings` directory\n- The global `window` object is not available in the `settings` component\n- Cannot use `window.fetch`, and instead must use the `hubspot.fetch` function provided by the `@hubspot/ui-extensions` npm package. Any urls called with the `hubspot.fetch` function must be added to the `config.permittedUrls.fetch` array in the `app` component's hsmeta.json file\n- Only components exported from the `@hubspot/ui-extensions` npm package can be used in `settings` components\n- React Components from `@hubspot/ui-extensions/crm` cannot be used in `settings` components\n\n# scim\n\n- There can only be one `scim` component\n- `scim` components must be in the `app/scim` directory\n\n# webhooks\n\n- There can only be one `webhooks` component.\n- `webhooks` components must be in the `app/webhooks` directory\n- `webhooks` components can only be in projects where `config.distribution` is private and `config.auth.type` is `static`\n\n### workflow-actions\n\n- `workflow-action` components must be in the `app/workflow-actions` directory\n\n## HubSpot CLI commands\n\n- All the commands and subcommands have a `--help` argument that provides details on the command and it's arguments\n- The help output is standard yargs output\n- The commands for working with projects in HubSpot are subcommands of `hs project`\n- Debugging flag that can be added to `hs` commands and subcommands: `--debug`\n- Debugging problems with CLI installation: `hs doctor`\n- `hs project open` will open the current project page in the browser\n- `hs init` is required to set up the hubspot configuration file\n- `hs auth` will authenticate a new account. This will require a user to open a browser and paste a token in a CLI prompt.\n- All the commands for managing HubSpot accounts in the CLI are subcommands of `hs account`\n\n## General\n\n- Follow existing patterns in the codebase\n- Use proper component structure based on component `type` in the `-hsmeta.json` file\n- Ensure configuration files follow HubSpot naming conventions\n- Always validate that components are placed in correct directories\n\n<!-- File: packages/cli/README.md (Tokens: ~502 | Category: plugin-manifest) -->\n# `dub-cli`\n\nA CLI for easily shortening URLs with the [Dub API](https://dub.co/api).\n\nhttps://github.com/user-attachments/assets/2ce9fe51-68ab-4e6d-b08d-4da09c17f90e\n\n## Available Commands\n\n| Command                   | Description                                                                                                                                                                                              |\n| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `dub login`               | Log into the DUB platform                                                                                                                                                                                |\n| `dub config`              | See your configured workspace credentials                                                                                                                                                                |\n| `dub domains`             | Configure your workspace domain                                                                                                                                                                          |\n| `dub shorten [url] [key]` | Create a short link. You can preemptively pass the URL and the generated short link key, or go through the CLI prompts.                                                                                  |\n| `dub links [options]`     | Search for links in your Dub workspace. Available options include: `-s, --search <search>` to search for a link by name, or `-l, --limit <limit>` to limit the number of links returned (default is 10). |\n| `dub help [command]`      | Display help for a specific command                                                                                                                                                                      |\n\n## Running Locally for Development\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package in watch mode:\n   ```bash\n   pnpm dev\n   ```\n3. In a separate terminal, navigate to the `cli` folder again and run an [available command](#available-commands):\n   ```bash\n   pnpm start [command]\n   ```\n4. See all [available commands](#available-commands) and options:\n   ```bash\n   pnpm start help\n   ```\n\n## Testing Production-like Setup\n\n> **Warning**\n> If you have previously installed `dub-cli` globally, uninstall it first to avoid conflicts\n\n1. Clone the repository, install dependencies and navigate to the `cli` folder:\n   ```bash\n   cd packages/cli\n   ```\n2. Build the package:\n   ```bash\n   pnpm build\n   ```\n3. Link the package globally:\n   ```bash\n    npm link\n   ```\n4. Verify the installation:\n   ```bash\n    dub -v\n   ```\n5. Run commands:\n   ```bash\n   dub [command]\n   ```\n\n<!-- File: packages/embeds/core/README.md (Tokens: ~68 | Category: plugin-manifest) -->\n# `@dub/embed-core`\n\n`@dub/embed-core` is a library that contains the core logic for embedding Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-core\n```\n\n## License\n\n[MIT](./LICENSE.md)\n\n<!-- File: packages/embeds/react/README.md (Tokens: ~68 | Category: plugin-manifest) -->\n# `@dub/embed-react`\n\n`@dub/embed-react` is a library of React components that are used embed Dub's referral dashboard on third-party websites.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/embed-react\n```\n\n## License\n\n[MIT](./LICENSE.md)\n\n<!-- File: packages/hubspot-app/README.md (Tokens: ~109 | Category: plugin-manifest) -->\n# Dub HubSpot App\n\nThis package contains the HubSpot app integration for Dub.\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Upload the project to HubSpot account\npnpm hs project upload --account=<account-id>\n\n# Set up a test environment and start local development\npnpm hs project dev\n\n# Install all project dependencies\npnpm hs project install-deps\n\n# Add new feature to app\npnpm hs project add\n\n# List all accounts connected\npnpm hs account list\n```\n\n<!-- File: packages/stripe-app/README.md (Tokens: ~129 | Category: plugin-manifest) -->\n# Stripe App\n\nThis is the [Stripe app](https://marketplace.stripe.com/apps/dub-conversions) for Dub Conversions.\n\n## Publish new version\n\n1. Run `stripe login` in your terminal and sign in to `Dub Technologies, Inc.`.\n2. Navigate to the `packages/stripe-app` directory.\n3. Increment the `version` field in `stripe-app.json`.\n4. Upload the updated app using `stripe apps upload`.\n5. Publish the new version via the [Stripe dashboard](https://dashboard.stripe.com/apps/dub.co).\n\n## Run locally\n\n```\nstripe apps start\n```\n\n<!-- File: packages/tinybird/README.md (Tokens: ~159 | Category: plugin-manifest) -->\n# Dub Tinybird Setup\n\n```bash\n├── datasources\n│   └── dub_click_events.datasource\n│   └── dub_click_events_mv.datasource\n│   └── dub_links_metadata.datasource\n│   └── dub_links_metadata_latest.datasource\n├── endpoints\n│   ├── browser.pipe\n│   ├── city.pipe\n│   ├── clicks.pipe\n│   ├── country.pipe\n│   ├── device.pipe\n│   ├── os.pipe\n│   └── referer.pipe\n│   └── timeseries.pipe\n│   └── top_links.pipe\n│   └── top_urls.pipe\n├── pipes\n│   ├── dub_click_events_pipe.pipe\n│   ├── dub_links_metadata_pipe.pipe\n```\n\n<!-- File: packages/ui/README.md (Tokens: ~51 | Category: plugin-manifest) -->\n# `@dub/ui`\n\n`@dub/ui` is a library of React components that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/ui\n```\n\n<!-- File: packages/ui/src/icons/nucleo/README.md (Tokens: ~97 | Category: plugin-manifest) -->\n# Nucleo Icons\n\n> _❗ These icons are copyrighted and redistribution is prohibited. See the full Nucleo copyright notice below._\n\nCopyright © Nucleo\n\nVersion 1.3, January 3, 2024\n\nNucleo Icons\n\nhttps://nucleoapp.com/\n\n- Redistribution of icons is prohibited.\n- Icons are restricted for use only within the product they are bundled with.\n\nFor more details:\n\nhttps://nucleoapp.com/license\n\n<!-- File: packages/utils/README.md (Tokens: ~51 | Category: plugin-manifest) -->\n# `@dub/utils`\n\n`@dub/utils` is a library of utility functions that are used across Dub's web applications.\n\n## Installation\n\nTo install the package, run:\n\n```bash\npnpm i @dub/utils\n```\n\n<!-- File: packages/workers/r2-object-created/README.md (Tokens: ~285 | Category: plugin-manifest) -->\n# r2-object-created Worker (thin forwarder)\n\nCloudflare Queue **push** consumer that forwards each R2 `object-create` notification to Next.js. Magic-byte validation, delete-on-mismatch, and Axiom logging run in the app.\n\n## Quick Edit\n\n1. Paste [`worker.js`](./worker.js) into Workers Quick Edit → Deploy.\n2. Settings → Bindings / Variables / Secrets (below).\n3. Confirm public R2 bucket **Event notification** `object-create` → matching queue.\n\n## Bindings\n\n| Type              | Name                               | Value                                                     |\n| ----------------- | ---------------------------------- | --------------------------------------------------------- |\n| Queues (consumer) | —                                  | `r2-object-created` or `-dev`                             |\n| Variable          | `DUB_WEBHOOK_URL`                  | `https://<host>/api/cloudflare/webhook/r2-object-created` |\n| Secret            | `CLOUDFLARE_WORKER_WEBHOOK_SECRET` | Same as Next.js `CLOUDFLARE_WORKER_WEBHOOK_SECRET`        |\n\nNo R2 or Axiom bindings on the Worker — Next.js uses `STORAGE_*` and Axiom already.\n\n## Flow\n\n```text\nR2 object-create → Queue → Worker → POST /api/cloudflare/webhook/r2-object-created\n  → magic-byte check → delete public object on mismatch → Axiom\n```\n</yakaai_skills>"}