diff --git a/.agents/hooks/git-policy.sh b/.agents/hooks/git-policy.sh new file mode 100755 index 000000000..30e4302bd --- /dev/null +++ b/.agents/hooks/git-policy.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# PreToolUse hook, matcher: Bash. Blocks force-push (incl. +refspec) and skipped git hooks. Allows --force-with-lease. +# Known false positive: a commit message containing " -n " is also blocked; the agent can rephrase. +cmd=$(jq -r '.tool_input.command // empty') +if echo "$cmd" | grep -Eq 'git +(push|commit).* --no-verify|git +push.*( -f| --force)( |$)|git +push.* \+[^ ]|git +commit.* -n( |$)'; then + echo "Blocked by repo policy: force-push and --no-verify are not allowed. Ask a human." >&2 + exit 2 +fi +exit 0 diff --git a/.agents/hooks/protect-files.sh b/.agents/hooks/protect-files.sh new file mode 100755 index 000000000..362c9b2c6 --- /dev/null +++ b/.agents/hooks/protect-files.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# PreToolUse hook, matcher: Edit|Write|MultiEdit|NotebookEdit. Blocks agent edits to protected paths. +# Known gap: edits made through Bash (sed -i, redirects) bypass this hook. +file=$(jq -r '.tool_input.file_path // .tool_input.notebook_path // empty') +[ -n "$file" ] || exit 0 +root=${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)} +rel=${file#"$root"/} +name=${rel##*/} +why= +case "$rel" in + lib/*|dist/*) why="generated build output; change src/ and run npm run build:node / build:web" ;; + package-lock.json) why="change it through npm install, not by hand" ;; + CHANGELOG.md|.pubnub.yml) why="written by the release bot from the PR description" ;; + test/specs/*) why="external checkout replaced by npm run test:contract:prepare" ;; + .agents/hooks/*|.pre-commit-config.yaml|.github/workflows/*) why="guardrail/CI configuration" ;; + secrets/*|*/secrets/*) why="may contain secrets" ;; +esac +case "$name" in .env|.env.*|*.env|*.pem|*.key) why="may contain secrets" ;; esac +if [ -n "$why" ]; then + echo "Blocked: $rel is protected ($why). Ask a human to change it." >&2 + exit 2 +fi +exit 0 diff --git a/.agents/hooks/quick-check.sh b/.agents/hooks/quick-check.sh new file mode 100755 index 000000000..104d23e90 --- /dev/null +++ b/.agents/hooks/quick-check.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Stop hook. Don't let the agent finish with a failing type check. Keep it under ~30 s. +# Stop hooks receive JSON on stdin; if this run was already continued by a hook, let the agent finish to avoid a loop. +[ "$(jq -r ".stop_hook_active // false" 2>/dev/null)" = true ] && exit 0 +cd "${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" || exit 0 +[ -n "$(git status --porcelain -- src test 2>/dev/null)" ] || exit 0 # no TypeScript changed +if [ ! -x node_modules/.bin/tsc ]; then + echo "quick-check skipped: node_modules missing (run npm ci)" >&2 + exit 0 +fi +if ! out=$(npm run -s typecheck 2>&1); then + echo "Type check failed. Fix it before finishing:" >&2 + echo "$out" | tail -n 40 >&2 + exit 2 +fi +exit 0 diff --git a/.agents/skills/sdk-architecture/SKILL.md b/.agents/skills/sdk-architecture/SKILL.md new file mode 100644 index 000000000..75b6379d8 --- /dev/null +++ b/.agents/skills/sdk-architecture/SKILL.md @@ -0,0 +1,47 @@ +--- +name: sdk-architecture +description: Use before changing, debugging, refactoring or reviewing SDK source under src/. Covers the request pipeline, platform and build boundaries, and per-module invariants and tests for publish, subscribe, presence, event engine and access manager. +--- + +# SDK architecture + +Load the module guide for the area you touch. For lifecycle changes, read subscribe and presence together. + +| Module | Read when the change touches | Guide | +| --- | --- | --- | +| Publish / signal | payload encoding, GET vs POST, encryption, store/TTL flags | [publish](modules/publish.md) | +| Subscriptions | entities, listeners, cursors, reconnect, SharedWorker | [subscribe](modules/subscribe.md) | +| Presence | heartbeat, leave, state, occupancy | [presence](modules/presence.md) | +| Event engine | state transitions, effects, cancellation | [event-engine](modules/event-engine.md) | +| Access Manager | grant/revoke, tokens, signatures | [access-manager](modules/access-manager.md) | + +## Request pipeline +`PubNubCore.sendRequest` (`src/core/pubnub-common.ts`) → an `AbstractRequest` subclass in `src/core/endpoints/`, which owns +validation, the operation identifier, path/query/body and response parsing → `PubNubMiddleware` +(`src/transport/middleware.ts`) → the platform transport. +- `sendRequest` owns validation, operation timeouts, status conversion and callback/Promise delivery. +- Middleware adds `instanceid`/`requestid`/`pnsdk`, auth key or token, signatures, and **request retries** + (`retryConfiguration`). Auth is skipped for `/v2/auth`, `/v3/pam` and `/time`; signing is skipped for `/time`. +- Copy a neighboring endpoint (`publish.ts`, `time.ts`) rather than inventing a new pipeline. +- Grouped APIs use facades: `pubnub-objects.ts`, `pubnub-data-sync.ts`, `pubnub-channel-groups.ts`, `pubnub-push.ts`. +- Public types live both in endpoint modules and in `src/core/types/api/`. Follow the actual export chain into + `lib/types/index.d.ts`, which `build:node-types` aggregates under the `PubNub` namespace. + +## Subscriptions +- `enableEventEngine` defaults to `true` (`src/event-engine/`). With `false`, the legacy `SubscriptionManager` runs + with the reconnection and deduplication managers. Both paths emit through `EventDispatcher`. +- Entities (`src/entities/`) and subscription sets can share one client subscription, so removing one owner must not + break another. +- In browsers, `subscriptionWorkerUrl` routes subscribe, heartbeat and leave through the SharedWorker + (`src/transport/subscription-worker/`), which is shared across tabs and clients. + +## Build boundaries +| Config | Compiles | Excludes | +| --- | --- | --- | +| `tsconfig.json` (`build:node`, `typecheck`) | core, node, react_native | web, worker, titanium, nativescript | +| `tsconfig.rollup.json` (`build:web`) | web + worker bundles | node, react_native, titanium, nativescript | + +- `process.env.*_MODULE` flags are replaced by `replaceConfiguration` (`rollup.config.js`) in **web bundles only**. + Worker bundles skip it, and the Node build evaluates the flags at runtime. A new flag needs an entry there; check the + build with the module both enabled and disabled. +- Node checks can't prove browser behavior. A browser-affecting change needs `build:web` plus the web suites. diff --git a/.agents/skills/sdk-architecture/modules/access-manager.md b/.agents/skills/sdk-architecture/modules/access-manager.md new file mode 100644 index 000000000..50b9336fa --- /dev/null +++ b/.agents/skills/sdk-architecture/modules/access-manager.md @@ -0,0 +1,7 @@ +# Access Manager + +Trace [access endpoints](../../../../src/core/endpoints/access_manager), [token manager](../../../../src/core/components/token_manager.ts), and [transport middleware](../../../../src/transport/middleware.ts). Distinguish legacy grant/audit from token grant/revoke and token consumption on normal requests. Signing credentials, tokens, auth keys, resource permissions, and authorized user identity are different concepts. + +Check permission serialization, TTL validation, signatures, error/status mapping, token precedence, and how auth changes affect subsequent subscribe/reconnect traffic. Do not introduce secret-key requirements into clients that only consume tokens; never put signing secrets in browser examples or diagnostics. + +Tests: [unit access tests](../../../../test/unit/access_manager), [access integration](../../../../test/integration/endpoints/access.test.ts), [grant token](../../../../test/integration/endpoints/grant_token.test.ts), [token manager](../../../../test/integration/components/token_manager.test.ts), and [contract definitions](../../../../test/contract/definitions/grant.ts). Contract setup: [contract.md](../../sdk-test/contract.md). diff --git a/.agents/skills/sdk-architecture/modules/event-engine.md b/.agents/skills/sdk-architecture/modules/event-engine.md new file mode 100644 index 000000000..4fe4d06f6 --- /dev/null +++ b/.agents/skills/sdk-architecture/modules/event-engine.md @@ -0,0 +1,7 @@ +# Event engine + +The [core engine](../../../../src/event-engine/core/engine.ts) processes transitions with context and queued events. [State definitions](../../../../src/event-engine/states) define subscribe states (handshaking, receiving, handshake_failed, receive_failed, handshake_stopped, receive_stopped, unsubscribed); [presence states](../../../../src/event-engine/presence) handle heartbeat/leave. [Dispatchers](../../../../src/event-engine/core/dispatcher.ts) execute managed effects and cancel them by invocation type. Core injects service calls, delays, and emitters. + +For a bug, capture initial state/context → incoming event → transition → effects → emitted status/message. Compare expected and actual traces. Review cancellation of obsolete requests/delays, retry termination (request retries live in src/transport/middleware.ts, not in states), retained cursors/channel sets, and disposal. Do not fix a state symptom without following the triggering API and effect callback. + +Use [event-engine tests](../../../../test/unit/event_engine.test.ts) and [presence-engine tests](../../../../test/unit/event_engine_presence.test.ts); despite their unit location, they exercise mocked HTTP flows. Add coverage for the failing transition and relevant interrupted/recovery path. Compare [legacy subscriptions](subscribe.md) when a public behavior must stay consistent. diff --git a/.agents/skills/sdk-architecture/modules/presence.md b/.agents/skills/sdk-architecture/modules/presence.md new file mode 100644 index 000000000..5fd7136eb --- /dev/null +++ b/.agents/skills/sdk-architecture/modules/presence.md @@ -0,0 +1,7 @@ +# Presence + +Presence spans [REST endpoints](../../../../src/core/endpoints/presence), core state/heartbeat APIs, [legacy subscriptions](../../../../src/core/components/subscription-manager.ts), [presence engine](../../../../src/event-engine/presence), and [worker managers](../../../../src/transport/subscription-worker). Occupancy queries, presence event subscriptions, heartbeat participation, and leave requests are distinct behaviors. + +Trace how channels/groups, presence timeout, heartbeat interval, state payloads, subscription changes, and disconnect/destroy affect traffic. In particular, offline cleanup and explicit unsubscribe need not produce the same leave behavior. Review [subscribe](subscribe.md) for shared ownership and worker coordination; a timing change can alter occupancy observed by other clients. + +Tests: [presence units](../../../../test/unit/presence), [presence endpoint](../../../../test/integration/endpoints/presence.test.ts), [heartbeat](../../../../test/integration/operations/heartbeat.test.ts), [state setting](../../../../test/integration/operations/stateSetting.test.ts), [subscribe heartbeat](../../../../test/integration/endpoints/subscribe_heartbeat.test.ts), and [presence engine](../../../../test/unit/event_engine_presence.test.ts). Reuse [presence mock helpers](../../../../test/utils.ts) and ensure timers/clients are cleaned up. diff --git a/.agents/skills/sdk-architecture/modules/publish.md b/.agents/skills/sdk-architecture/modules/publish.md new file mode 100644 index 000000000..e6b28f756 --- /dev/null +++ b/.agents/skills/sdk-architecture/modules/publish.md @@ -0,0 +1,7 @@ +# Publish and signal + +Trace [PubNubCore](../../../../src/core/pubnub-common.ts) → [PublishRequest](../../../../src/core/endpoints/publish.ts) or [SignalRequest](../../../../src/core/endpoints/signal.ts) → [middleware](../../../../src/transport/middleware.ts) → platform transport. Preserve the distinction between GET URL payloads and POST bodies, encoding/serialization, encryption, history/TTL flags, metadata, custom message types, and string timetokens. + +Read the supplied endpoint contract before changing accepted inputs: existing truthiness checks and documented payload types may disagree, and fixing that is an observable behavior change. Check callback and Promise delivery as well as request bytes. + +Start with [unit publish tests](../../../../test/unit/publish/publish.test.ts), [integration publish tests](../../../../test/integration/endpoints/publish.test.ts), and [signal tests](../../../../test/integration/endpoints/signal.test.ts). Add boundary/error cases to the nearest suite; use mocked HTTP before considering live publish traffic. diff --git a/.agents/skills/sdk-architecture/modules/subscribe.md b/.agents/skills/sdk-architecture/modules/subscribe.md new file mode 100644 index 000000000..32cbcadb3 --- /dev/null +++ b/.agents/skills/sdk-architecture/modules/subscribe.md @@ -0,0 +1,9 @@ +# Subscriptions + +Trace [PubNubCore](../../../../src/core/pubnub-common.ts), [entities](../../../../src/entities), [EventDispatcher](../../../../src/core/components/event-dispatcher.ts), and the selected implementation. `enableEventEngine` (default `true`) selects [event engine](../../../../src/event-engine); otherwise [SubscriptionManager](../../../../src/core/components/subscription-manager.ts) uses reconnection and deduplication helpers. + +Preserve channel/group ownership, subscribe cursors, reconnect behavior, message categorization, listener delivery, and unsubscribe cleanup. Entity subscriptions and subscription sets can share client subscriptions; removing one owner's listener/subscription must be assessed against others. Review both implementations rather than assuming behavior is identical. + +Browser `subscriptionWorkerUrl` introduces [worker middleware and managers](../../../../src/transport/subscription-worker), which coordinate subscribe/heartbeat/leave across clients. Consult [presence](presence.md) whenever changing lifecycle or subscription state. + +Tests: [subscribe endpoint](../../../../test/integration/endpoints/subscribe.test.ts), [legacy manager](../../../../test/integration/components/subscription_manager.test.ts), [event engine](../../../../test/unit/event_engine.test.ts), [unsubscribe](../../../../test/integration/operations/unsubscribe.test.ts), and [SharedWorker](../../../../test/integration/shared-worker/shared-worker.test.ts). The latter uses real demo-key traffic; see the [sdk-test](../../sdk-test/SKILL.md) skill. diff --git a/.agents/skills/sdk-debug/SKILL.md b/.agents/skills/sdk-debug/SKILL.md new file mode 100644 index 000000000..67d2f4cee --- /dev/null +++ b/.agents/skills/sdk-debug/SKILL.md @@ -0,0 +1,19 @@ +--- +name: sdk-debug +description: Use when investigating a PubNub JavaScript SDK bug, regression, flaky behavior or customer-reported issue. Covers reproduction, root-cause classification, regression test first, then the narrow fix. +--- + +# sdk-debug + +1. **Evidence first.** Gather the SDK version, platform/runtime, configuration (with secrets removed), and expected vs + actual behavior. Work from the evidence provided; ask only for inputs that block diagnosis. +2. **Classify the root cause** as one of: service or configuration, contract mismatch, timing/resource leak, or SDK + defect. Don't present an unexecuted reproduction as confirmed. +3. **Trace** with the module guide from [sdk-architecture](../sdk-architecture/SKILL.md): + public API → endpoint or subscribe/presence path → middleware → transport. For the event engine, record + `state + context → event → transition → effects → emitted status` and compare the expected trace with the actual one. + Check the legacy manager and SharedWorker paths only where they're implicated. +4. **Regression test first** ([sdk-test](../sdk-test/SKILL.md)), then the narrowest fix. Record the run before and after + the fix. If tests can't run, say the hypothesis is unverified. +5. A fix can break customers who rely on the old behavior. Report the root cause, the behavior change and who it affects, + using the four report fields from `AGENTS.md`. diff --git a/.agents/skills/sdk-deps/SKILL.md b/.agents/skills/sdk-deps/SKILL.md new file mode 100644 index 000000000..c65bc44b7 --- /dev/null +++ b/.agents/skills/sdk-deps/SKILL.md @@ -0,0 +1,19 @@ +--- +name: sdk-deps +description: Use when upgrading, adding or auditing PubNub JavaScript SDK npm dependencies (npm outdated, npm audit, Dependabot-style bumps). Covers lockfile handling, bundle and platform impact, and validation. +--- + +# sdk-deps + +1. A new runtime dependency needs approval first. Bumps of existing dependencies don't. +2. Change dependencies only through npm, e.g. `npm install @`. `package-lock.json` is hook-protected + from hand edits. Check whether the `overrides` block in `package.json` still applies. +3. Check platform impact: + - Runtime dependencies ship to Node, browsers and React Native. + - Verify browser/RN compatibility (no Node built-ins in shared code) and any polyfill requirements. +4. Check bundle impact: run `npm run build:web` and compare the `dist/web/pubnub.min.js` size against `master`. Report + any growth. +5. Validate with `.scripts/agent-test.sh node`, and with `web` when browser code is affected. Each source PR also + commits rebuilt artifacts ([sdk-release](../sdk-release/SKILL.md)). +6. Triaging `npm outdated` or `npm audit` output is mechanical; give it to a light-model subagent. Report the + versions changed and the risk. diff --git a/.agents/skills/sdk-feature/SKILL.md b/.agents/skills/sdk-feature/SKILL.md new file mode 100644 index 000000000..31342f175 --- /dev/null +++ b/.agents/skills/sdk-feature/SKILL.md @@ -0,0 +1,24 @@ +--- +name: sdk-feature +description: Use when adding or enhancing a PubNub JavaScript SDK feature, endpoint, parameter or public API. Contract-first steps, public-surface checklist and validation. +--- + +# sdk-feature + +1. **Contract first.** There are no OpenAPI specs in this repo. Before changing request or response semantics, ask + for the operation's spec (operationId/path and revision) and record it in the PR. If the spec is missing or contradicts + current behavior, stop and surface the gap. Gherkin scenarios in `sdk-specifications` describe SDK behavior; they + aren't schemas. +2. Load [sdk-architecture](../sdk-architecture/SKILL.md) and the relevant module guide. Copy the nearest existing endpoint + or facade method rather than designing a new shape. +3. Public surface: + - callback and Promise overloads + - JSDoc with defaults, an example and any deprecation + - `@internal` on anything non-public + - export chain through to `lib/types/index.d.ts` + - validation and error messages consistent with neighboring APIs +4. Optional feature module? Add the `process.env._MODULE` guard and its `replaceConfiguration` entry. +5. Tests, via [sdk-test](../sdk-test/SKILL.md): validation boundaries, the encoded request, response/error mapping, + and both callback and Promise forms. +6. `npm run build:node`, then `.scripts/agent-test.sh release` (declaration checks). +7. Report with the four report fields from `AGENTS.md`. Call out new network traffic, bundle size and any platform restrictions. diff --git a/.agents/skills/sdk-refactor/SKILL.md b/.agents/skills/sdk-refactor/SKILL.md new file mode 100644 index 000000000..754871597 --- /dev/null +++ b/.agents/skills/sdk-refactor/SKILL.md @@ -0,0 +1,22 @@ +--- +name: sdk-refactor +description: Use when restructuring PubNub JavaScript SDK code without intending behavior change. Covers the invariant checklist, characterization tests and a deterministic public-type-surface gate. +--- + +# sdk-refactor + +1. **Public type surface must not change.** Run `npm run build:node && git diff --exit-code lib/types/index.d.ts` + before you finish. A non-empty diff means a public-API change: stop and report it, or revert it. +2. Map callers and the affected modules ([sdk-architecture](../sdk-architecture/SKILL.md)). Keep platform boundaries + and module guards intact. +3. Write down the invariants that apply: + - overloads and types + - defaults, validation, error categories and messages + - event order and timing + - retry and cancellation + - network traffic (heartbeat, leave) + - cleanup of timers and listeners +4. Rely on existing tests. Add characterization tests only for invariants that nothing currently covers, and run them + before and after the change. +5. Don't mix behavior fixes into a refactor; list any you find as follow-ups. +6. Report every observable difference, however small. "Refactor" alone isn't a compatibility claim. diff --git a/.agents/skills/sdk-release/SKILL.md b/.agents/skills/sdk-release/SKILL.md new file mode 100644 index 000000000..c8b174b67 --- /dev/null +++ b/.agents/skills/sdk-release/SKILL.md @@ -0,0 +1,19 @@ +--- +name: sdk-release +description: Use when finalizing a PubNub JavaScript SDK PR for merge or release. Covers rebuilding the committed lib/ and dist/web artifacts, verifying release consistency, and writing the PR description the changelog bot reads. +--- + +# sdk-release + +1. **Never edit versions or changelogs.** A GitHub bot writes the version strings, `CHANGELOG.md` and `.pubnub.yml` + from the PR description. Its version targets are listed in `.github/workflows/release/versions.json`. + `.agents/hooks/protect-files.sh` blocks edits to the changelog files. Feature work never bumps the version. +2. Run `npm run build`. It regenerates `lib/` and `dist/web/`, and every source PR commits both. + Review the source diff first, then sanity-check `git diff --stat lib dist` for unexpected files. +3. Run `.scripts/agent-test.sh release`. It checks that `package.json`, `getVersion()` and the README CDN links agree, + and type-checks the aggregated declarations. +4. Write the PR title and body in the format from `.github/pull_request_template.md`: `type(scope): subject`, a body, + then a footer. The bot builds the changelog from them, so state the user-visible change precisely. Use `Closes #n` + for issues addressed and `BREAKING CHANGES:` for any breaking change. +5. Publishing to npm, S3 and GitHub releases happens on merge (`release.yml`). Never run + `.github/workflows/release/*.sh` or `npm publish`. diff --git a/.agents/skills/sdk-review/SKILL.md b/.agents/skills/sdk-review/SKILL.md new file mode 100644 index 000000000..cea6b39e8 --- /dev/null +++ b/.agents/skills/sdk-review/SKILL.md @@ -0,0 +1,23 @@ +--- +name: sdk-review +description: Use when reviewing a PubNub JavaScript SDK diff or PR. Covers an SDK-specific compatibility checklist, finding format, and the confirmed-versus-uncertain split. +--- + +# sdk-review + +1. Read the diff and its callers and tests. Review source before generated `lib/` and `dist/`; look at those only to + confirm build or export consistency. +2. Check the SDK-specific risks. Linting covers style, so don't raise style findings. + - **Public types:** removed or renamed exports, overload resolution, newly required params, return types, + `@internal` leaks into `lib/types/index.d.ts`. + - **Behavior:** validation, defaults, null/undefined handling, error category and message, callback vs Promise + agreement and timing. + - **Realtime lifecycle:** ordering/dedup, retry/backoff, reconnect cursors, listener delivery, heartbeat/leave traffic, + cleanup on unsubscribe/destroy, both event-engine and legacy paths, SharedWorker clients. + - **Platform and bundle:** Node/browser-only imports in shared core, feature-flag guards, bundle size, new silent + network traffic. + - **Contract:** the wire format matches a supplied spec, or the PR states the spec is missing. + - **Tests:** cover the changed behavior. A test that only mirrors implementation doesn't count. +3. For each finding give the location, trigger, customer consequence and severity. Keep confirmed defects separate + from uncertainties. +4. End with the four report fields from `AGENTS.md`. "No findings" isn't proof of compatibility. diff --git a/.agents/skills/sdk-test/SKILL.md b/.agents/skills/sdk-test/SKILL.md new file mode 100644 index 000000000..1c4b977b4 --- /dev/null +++ b/.agents/skills/sdk-test/SKILL.md @@ -0,0 +1,47 @@ +--- +name: sdk-test +description: Use when choosing, writing or running PubNub JavaScript SDK tests, including browser (karma), live-key feature and contract (cucumber) suites. Covers the suite map, prerequisites, env vars and how to report results. +--- + +# sdk-test + +Always run tests through `.scripts/agent-test.sh`. It prints one line on pass and only the failures on fail, and it +blocks with exit 2 when a prerequisite is missing. Running a suite and summarizing it is mechanical, so it can go to a +light-model subagent that returns the wrapper output verbatim. + +## Pick the smallest suite +| Run | Covers | Needs | Network | +| --- | --- | --- | --- | +| `.test.ts [--grep re]` | one mocha file | `npm install` | per file (most use nock) | +| `unit` | `test/unit` | `npm install` | mocked; **no keys needed** | +| `integration` | `test/integration` (minus shared-worker) | `npm install` | mocked, **no keys needed** except `endpoints/data_sync/` (35 tests need `DS_*` keys) | +| `release` | version consistency + `lib/types` declarations | `build:node` | none | +| `node` | unit + integration + release (= `npm run test:node`) | `build:node` | mocked | +| `web` | `test/dist/web-titanium.test.js` in ChromeHeadless | `build:web`, Chrome, port 9898 | **live**, demo keys | +| `shared-worker` | `test/integration/shared-worker` | `build:web`, Chrome, port 9876 | **live**, demo keys | +| `feature-files` / `feature-objects` | file upload / App Context | `PUBLISH_KEY`, `SUBSCRIBE_KEY` with Files / App Context enabled | **live**; creates or mutates resources. Ask first | +| `contract` | cucumber scenarios | see [contract.md](contract.md) | mock on :8090 | + +- `test/unit/event_engine*.test.ts` drive full clients over nock-mocked HTTP. +- `npm test` includes live browser traffic; it is not an offline smoke test. +- `ci:*` scripts clean, build, lint and test everything. Use them only for full validation, never concurrently. +- Run one suite at a time. Suites share ports, fake timers, mocks and build directories. + +## Secrets +- Env var names: `PUBLISH_KEY`, `SUBSCRIBE_KEY`, plus `PAM_{PUBLISH,SUBSCRIBE,SECRET}_KEY` and `DS_{PUBLISH,SUBSCRIBE,SECRET}_KEY`. + CI supplies these. `unit`, and `integration` outside `endpoints/data_sync/`, run without them. `data_sync` needs the + `DS_*` keys, and `node` includes it, so it needs them too. +- Load them inside the same command: `set -a; . test/.env.local; set +a; .scripts/agent-test.sh feature-files` + (`test/.env.local` is gitignored). +- Never print values. Never pass real signing secrets to browser tests. The wrapper redacts secret-looking env values + from its output. + +## Results +- Exit codes: `0` pass · `1` (or the runner's code) fail · `2` blocked or no tests ran. +- On failure, the wrapper prints the full log's path. `grep` it; don't read it whole. +- Zero tests, entirely skipped, blocked or unrun suites are validation gaps, never passes. Report the exact commands + you ran and what you didn't run. +- Browser demo-key passes don't prove production delivery behavior. + +## Writing tests +Follow [authoring.md](authoring.md). diff --git a/.agents/skills/sdk-test/authoring.md b/.agents/skills/sdk-test/authoring.md new file mode 100644 index 000000000..5c2508c53 --- /dev/null +++ b/.agents/skills/sdk-test/authoring.md @@ -0,0 +1,30 @@ +# Writing SDK tests + +Start from the nearest existing test for the endpoint or component and keep its Chai/Sinon/Nock style. Test +customer-visible behavior, not implementation details. + +| Change | Cover | +| --- | --- | +| REST feature/fix | validation boundaries, encoded path/query/body, response/error mapping, callback and Promise forms | +| Subscribe/presence | legacy and event-engine paths, channel/group ownership, status/message order, reconnect/cancel, heartbeat/leave, cleanup | +| Public typings | declaration aggregation (`release` suite), overload assignability | +| Bug | minimal failing reproduction plus adjacent behavior the fix could affect | + +**Helpers** (`test/utils.ts`): +- `createNock()` matches `ps*.pndsn.com`. +- `createSubscribeMockScopes` and `createPresenceMockScopes` build subscribe and presence mocks. + +Use these helpers instead of copying protocol fixtures. Assert that the expected interceptors were consumed. + +**Client setup and teardown:** +- Tests unrelated to retries set `retryConfiguration: PubNub.NoneRetryPolicy()`. Otherwise a mocked 5xx keeps retrying + past mocha's 5 s timeout. +- Retry tests must set the retry policy they exercise explicitly. +- Tear down with `pubnub.destroy(true)` to avoid leave traffic, unless leave behavior is under test. +- Restore fake timers and stubs, remove listeners, and destroy clients even when assertions fail (use `afterEach`). + +**Determinism:** +- Prefer deterministic completion signals or fake timers over sleeps. +- Timetokens exceed safe-integer precision; keep them as strings. +- Never add a live-service dependency to a mocked test, or use a real key to stand in for a missing nock mock. +- Don't add mocha parallel mode. diff --git a/.agents/skills/sdk-test/contract.md b/.agents/skills/sdk-test/contract.md new file mode 100644 index 000000000..49a8aa87e --- /dev/null +++ b/.agents/skills/sdk-test/contract.md @@ -0,0 +1,22 @@ +# Contract tests (cucumber) + +Ask before starting: setup clones private repos over SSH and **deletes and replaces** its destination directories. + +| Source | Local path (gitignored) | Command | +| --- | --- | --- | +| [sdk-specifications](https://github.com/pubnub/sdk-specifications) | `test/specs` | `npm run test:contract:prepare` (rm + shallow clone of `master`) | +| [service-contract-mock](https://github.com/pubnub/service-contract-mock) | `dist/contract` | `npm run contract:refresh` (rm + clone + npm install) | + +1. Run `npm run test:contract:prepare`, then `npm run contract:refresh`. +2. Record the resolved commits in the report: `git -C test/specs rev-parse HEAD` and `git -C dist/contract rev-parse HEAD`. +3. Run `npm run build:node`. +4. Start the mock in the background and leave it running: `npm run contract:server`. It must own port 8090 exclusively. + Another checkout using the same server will interfere. +5. Run the default access scenario with `.scripts/agent-test.sh contract`, or pick scenarios with + `.scripts/agent-test.sh contract --tags '@featureSet=access and not @na=js and not @skip'`. +6. Stop the mock server when you're done. + +- `test/contract/shared/hooks.ts` initializes the scenario state at `localhost:8090`. +- PAM helpers carry fixed fallback keys for the mock, so no real keys are needed. +- The older `contract:test*` scripts use a different compiled setup (`dist/cucumber`). Don't mix the two paths. +- Gherkin scenarios describe SDK behavior; they don't establish an OpenAPI schema. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..cbfb1fb01 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,25 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit|NotebookEdit", + "hooks": [ + { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.agents/hooks/protect-files.sh" } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.agents/hooks/git-policy.sh" } + ] + } + ], + "Stop": [ + { + "hooks": [ + { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.agents/hooks/quick-check.sh" } + ] + } + ] + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..b1bed472b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ + + +feat(scope): subject + +Body: what changed and why. Validation: commands run and results. Skipped: checks not run and why. + +Closes #issue +BREAKING CHANGES: what changed (only if breaking) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 000000000..d9a312aeb --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,40 @@ +name: Pre-commit checks + +# Same hooks as .pre-commit-config.yaml. Local hooks can be skipped with --no-verify; this job can't. +on: + pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +defaults: + run: + shell: bash + +jobs: + pre-commit: + name: Formatting, lint and secret scan + runs-on: + group: organization/Default + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup NodeJS 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + - name: Install dependencies + run: npm ci + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Run pre-commit on changed files + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + pip install pre-commit + pre-commit run --show-diff-on-failure --color=never --from-ref "$BASE_SHA" --to-ref HEAD diff --git a/.gitignore b/.gitignore index 77ff72cfd..50d79a1d2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ dist/contract dist/cucumber upload test/specs -.claude/* # GitHub Actions # ################## @@ -24,3 +23,6 @@ test/specs release-artifacts-* .vscode + +.env +.env.local diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..4d8778bc0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +# Pre-commit: formatting, lint fixes, secret scan. CI runs the same hooks (.github/workflows/pre-commit.yml). +# Setup: pip install pre-commit && pre-commit install. Bump revs with `pre-commit autoupdate`. +exclude: ^(lib|dist|upload)/|^package-lock\.json$|^CHANGELOG\.md$|^\.pubnub\.yml$ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-yaml + - id: check-added-large-files + - id: detect-private-key + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.1 + hooks: + - id: gitleaks + - repo: local + hooks: + - id: eslint + name: eslint --fix (includes prettier) on src + entry: npx --no-install eslint --fix --config .eslintrc.cjs + language: system + files: ^src/.*\.ts$ + - id: prettier + name: prettier on tests + entry: npx --no-install prettier --write + language: system + files: ^test/.*\.(ts|js)$ + - id: agent-docs + name: agent harness checks + entry: .scripts/check-agent-docs.sh + language: script + pass_filenames: false + files: ^(AGENTS\.md|\.agents/|\.scripts/) diff --git a/.scripts/agent-test.sh b/.scripts/agent-test.sh new file mode 100755 index 000000000..d02e0ef69 --- /dev/null +++ b/.scripts/agent-test.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Run a test file, a suite, or any noisy command with filtered output. +# .scripts/agent-test.sh test/unit/publish/publish.test.ts [--grep ''] +# .scripts/agent-test.sh unit|integration|release|node|web|shared-worker|feature-files|feature-objects|contract [--tags ''] +# .scripts/agent-test.sh e.g. npm run build:node +# Pass: one line. Fail: matching error lines (<=150) + last 30 lines + path of the full log. +# Exit: the command's status; 2 = blocked (missing prerequisite) or no tests ran. +set -u +cd "$(dirname "$0")/.." || exit 2 + +blocked() { echo "BLOCKED: $*"; exit 2; } +need() { [ -e "$1" ] || blocked "$1 missing: $2"; } +need_env() { for v in "$@"; do [ -n "${!v:-}" ] || blocked "env $v not set (load test/.env.local, see sdk-test skill)"; done; } +redact() { # mask values of secret-looking env vars in stdin + local text name val + text=$(cat) + for name in $(env | sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' | grep -Ei 'key|token|secret|password'); do + val=${!name:-} + [ ${#val} -ge 6 ] && text=${text//"$val"/[REDACTED:$name]} + done + printf '%s\n' "$text" +} + +[ $# -gt 0 ] || { sed -n '2,7p' "$0"; exit 2; } +major=$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null) +case "$major" in 22|24) ;; *) echo "WARN: Node ${major:-not found}; CI uses 22/24. Startup failures on other majors are environmental." ;; esac + +export TS_NODE_PROJECT=./tsconfig.json +# --no-config: otherwise .mocharc.json's spec glob adds the whole suite to an explicit file selection. +MOCHA="npx --no-install mocha --project tsconfig.mocha.json --require tsx --no-config --file test/setup-why.ts --timeout 5000 --reporter dot" +label="$*" +tests=1 +case "$1" in + *.test.ts) + need node_modules "run npm install"; need "$1" "pass a path relative to the repo root" + f=$1; shift; set -- $MOCHA "$@" "$f" ;; + unit|integration|release) + need node_modules "run npm install" + [ "$1" = release ] && need lib/types/index.d.ts "run npm run build:node" + s=$1; shift; set -- $MOCHA --exclude 'test/integration/shared-worker/**' "$@" "test/$s/**/*.test.ts" ;; + node) + need node_modules "run npm install"; need lib/types/index.d.ts "run npm run build:node" + shift; set -- npm run -s test:node -- --reporter dot "$@" ;; + web) + need node_modules "run npm install"; need dist/web/pubnub.js "run npm run build:web" + set -- npm run -s test:web ;; + shared-worker) + need node_modules "run npm install"; need dist/web/pubnub.worker.js "run npm run build:web" + set -- npm run -s test:web:shared-worker ;; + feature-files) + need node_modules "run npm install"; need_env PUBLISH_KEY SUBSCRIBE_KEY + set -- npm run -s test:feature:fileupload:node ;; + feature-objects) + need node_modules "run npm install"; need_env PUBLISH_KEY SUBSCRIBE_KEY + set -- npm run -s test:feature:objectsv2:node ;; + contract) + need node_modules "run npm install"; need test/specs "see .agents/skills/sdk-test/contract.md" + lsof -nP -iTCP:8090 -sTCP:LISTEN >/dev/null 2>&1 || blocked "no mock server on :8090 (npm run contract:server)" + if [ "${2:-}" = --tags ]; then set -- npx --no-install cucumber-js -p default --tags "${3:?--tags needs a value}" + else set -- npm run -s test:contract:start; fi ;; + *) tests=0 ;; +esac + +log=$(mktemp "${TMPDIR:-/tmp}/agent-test.XXXXXX") +"$@" >"$log" 2>&1 +status=$? +lines=$(wc -l <"$log" | tr -d ' ') +if [ "$tests" = 1 ] && [ "$status" = 0 ] && grep -Eq '^ *0 passing|Executed 0 of 0|^0 scenarios' "$log"; then + echo "FAIL: no tests ran: $label (full log: $log)" + exit 2 +fi +if [ "$status" = 0 ]; then + echo "PASS: $label ($lines lines of output suppressed)" + rm -f "$log" + exit 0 +fi +echo "FAIL (exit $status): $label" +{ + grep -E -A 8 '(failing|AssertionError|Error:|ERROR|FAILED|✗|✕|^ +[0-9]+\) )' "$log" | head -n 150 + echo "--- last 30 lines ---" + tail -n 30 "$log" +} | redact +echo "Full log ($lines lines): $log (grep/tail it; don't read it whole)" +exit "$status" diff --git a/.scripts/check-agent-docs.sh b/.scripts/check-agent-docs.sh new file mode 100755 index 000000000..f903f15c4 --- /dev/null +++ b/.scripts/check-agent-docs.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Lint the agent harness: size limits, skill frontmatter, relative links, referenced paths and npm scripts. +# Runs in pre-commit when AGENTS.md, .agents/ or .scripts/ change. Prints OK or one line per error. +cd "$(dirname "$0")/.." || exit 2 +err=0 +fail() { echo "ERROR: $*"; err=1; } + +files="AGENTS.md $(find .agents -name '*.md' | sort)" +[ "$(wc -l ']` +- Keyless tests: `.scripts/agent-test.sh unit` runs with no env keys; nock mocks all HTTP. `integration` also passes without keys, except `test/integration/endpoints/data_sync/` (35 tests), which needs the `DS_*` keys. +- Other suites: `release` needs `build:node`. `node` (unit + integration + release) needs `build:node` and the `DS_*` keys. Browser, live-key and contract suites → sdk-test skill, ask first. +- Wrap any noisy command: `.scripts/agent-test.sh npm run build:node` prints one line on pass and only the failures on fail. +- Don't call mocha/karma/cucumber directly: mocha without `--no-config` silently runs the whole suite. +- Inspect logs with `grep`/`tail`; never read a full log. +- Formatting, lint fixes and the secret scan run in pre-commit. Don't run them manually. + +## Layout +- `src/core/`: shared public API. `pubnub-common.ts` (PubNubCore), `endpoints/` (one `AbstractRequest` subclass per REST call), `components/`, `types/api/` +- `src/event-engine/`: subscribe/presence state machines, on by default. `enableEventEngine: false` uses `core/components/subscription-manager.ts` instead +- `src/transport/`: platform transports. `middleware.ts` owns auth, signatures and request retries; `subscription-worker/` is the browser SharedWorker +- `src/{node,web,react_native}/`: entry points. No build config compiles `src/{titanium,nativescript}/`. +- `lib/`, `dist/web/`: committed build output. Regenerate it with the build commands; never edit or search it. +- `test/unit`, `test/integration`: mocha + chai/sinon/nock with network disabled; helpers in `test/utils.ts` +- `test/dist`, `test/feature`, `test/integration/shared-worker`: live-network suites, excluded from `.mocharc.json` + +## Conventions that differ from the defaults +- Timetokens are strings (they exceed 2^53). Never convert them to numbers. +- Public methods expose callback and Promise overloads; keep both when changing a signature. +- Optional modules are gated by `process.env._MODULE`. Rollup's `replaceConfiguration` substitutes them in web + bundles only (PAM is off by default there); Node evaluates them at runtime. Add any new flag to `replaceConfiguration`. +- `src/core`, `src/entities` and `src/event-engine` must not import Node- or browser-only APIs. +- Mark non-public declarations `@internal`; `stripInternal` drops them from the published types. +- Validation, defaults, error text and null/undefined handling are customer-visible. Never "fix" them as incidental cleanup. + +## Boundaries +- **Always:** end every change or review with these four fields: + - breaking change (yes/no/uncertain, with evidence) + - behavior change, however small + - validation actually run vs skipped + - customer impact + + A skipped or zero-test run is not a pass. PR titles and bodies follow `.github/pull_request_template.md`. +- **Always:** when you change a command, script or test prerequisite, update this file or the affected skill in the same change. +- **Always:** run one test or build command at a time (`ci:*` and builds wipe `lib/` and `dist/`). Refer to key env vars by name; never print their values. +- **Ask first:** public API signature or type changes · new dependencies · live-network or real-key suites · endpoint wire-format changes. There are no OpenAPI specs in this repo: ask for the operation's spec rather than inferring it from code. +- **Never** (each is enforced by a hook or CI): + - hand-edit `lib/`, `dist/`, `package-lock.json`, `CHANGELOG.md`, `.pubnub.yml`, `.env*` or key files → `.agents/hooks/protect-files.sh` + - force-push or use `--no-verify` → `.agents/hooks/git-policy.sh` + - commit secrets → gitleaks in pre-commit and CI + - finish with a failing typecheck → `.agents/hooks/quick-check.sh` + +## More context +Skills live in `.agents/skills/`. Read only the one the task needs. +- Internals, per-module invariants and test locations → [sdk-architecture](.agents/skills/sdk-architecture/SKILL.md) +- Feature or endpoint → [sdk-feature](.agents/skills/sdk-feature/SKILL.md) · Bug → [sdk-debug](.agents/skills/sdk-debug/SKILL.md) +- Refactor → [sdk-refactor](.agents/skills/sdk-refactor/SKILL.md) · Review → [sdk-review](.agents/skills/sdk-review/SKILL.md) +- Choosing, writing or running tests → [sdk-test](.agents/skills/sdk-test/SKILL.md) +- Finalizing a PR (rebuild artifacts, release bot) → [sdk-release](.agents/skills/sdk-release/SKILL.md) +- Dependency upgrade → [sdk-deps](.agents/skills/sdk-deps/SKILL.md) +- Delegate mechanical work (running suites, log triage, file moves, `.scripts/check-agent-docs.sh`) to a subagent on a + light model (e.g. Haiku) that returns only a summary. Keep design, debugging and review on the main model. diff --git a/package-lock.json b/package-lock.json index f393329b7..dafe730b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7879,9 +7879,9 @@ "license": "Apache-2.0" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 40e5b5112..d406315e7 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "contract:test-beta": "cucumber-js --require dist/cucumber dist/contract/contract/features --tags 'not @na=js and @beta'", "contract:test-access": "cucumber-js --require dist/cucumber dist/contract/contract/features --tags '@featureSet=access and not @na=js and not @beta'", "contract:test-objectsV2": "cucumber-js --require dist/cucumber dist/contract/contract/features --tags '@featureSet=objectsV2 and not @na=js and not @beta'", - "contract:test-access-beta": "cucumber-js --require dist/cucumber dist/contract/contract/features --tags '@featureSet=access and not @na=js and @beta'" + "contract:test-access-beta": "cucumber-js --require dist/cucumber dist/contract/contract/features --tags '@featureSet=access and not @na=js and @beta'", + "typecheck": "tsc -p tsconfig.json --noEmit" }, "main": "./lib/node/index.js", "types": "./lib/types/index.d.ts", @@ -148,4 +149,4 @@ "engines": { "node": ">=22.0.0" } -} \ No newline at end of file +}