@intelmesh/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.github/scripts/compute-disttag.sh +47 -0
  2. package/.github/workflows/release.yml +206 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +2 -0
  5. package/.prettierrc +8 -0
  6. package/CLAUDE.md +37 -0
  7. package/LICENSE +21 -0
  8. package/commitlint.config.cjs +3 -0
  9. package/dist/index.d.ts +1293 -0
  10. package/dist/index.js +1651 -0
  11. package/docs/superpowers/plans/2026-04-10-release-pipeline.md +798 -0
  12. package/docs/superpowers/specs/2026-04-10-release-pipeline-design.md +309 -0
  13. package/eslint.config.mjs +38 -0
  14. package/package.json +72 -0
  15. package/src/builders/event.ts +72 -0
  16. package/src/builders/rule.ts +143 -0
  17. package/src/client/errors.ts +171 -0
  18. package/src/client/http.ts +209 -0
  19. package/src/client/intelmesh.ts +57 -0
  20. package/src/client/pagination.ts +50 -0
  21. package/src/generated/types.ts +11 -0
  22. package/src/index.ts +106 -0
  23. package/src/provision/index.ts +6 -0
  24. package/src/provision/provisioner.ts +326 -0
  25. package/src/provision/rule-builder.ts +193 -0
  26. package/src/resources/apikeys.ts +63 -0
  27. package/src/resources/audit.ts +29 -0
  28. package/src/resources/evaluations.ts +38 -0
  29. package/src/resources/events.ts +61 -0
  30. package/src/resources/lists.ts +91 -0
  31. package/src/resources/phases.ts +71 -0
  32. package/src/resources/rules.ts +98 -0
  33. package/src/resources/scopes.ts +71 -0
  34. package/src/resources/scores.ts +63 -0
  35. package/src/testkit/assertion.ts +76 -0
  36. package/src/testkit/harness.ts +252 -0
  37. package/src/testkit/index.ts +7 -0
  38. package/src/types.ts +330 -0
  39. package/tests/client/errors.test.ts +159 -0
  40. package/tests/provision/provisioner.test.ts +311 -0
  41. package/tests/scripts/compute-disttag.test.ts +178 -0
  42. package/tests/testkit/harness.test.ts +291 -0
  43. package/tsconfig.eslint.json +8 -0
  44. package/tsconfig.json +29 -0
  45. package/vitest.config.ts +14 -0
@@ -0,0 +1,309 @@
1
+ # Release Pipeline Design — `@intelmesh/sdk`
2
+
3
+ **Date:** 2026-04-10
4
+ **Status:** Draft (pending user review)
5
+ **Scope:** GitHub Actions workflow to publish `@intelmesh/sdk` to the public npm registry and create a GitHub Release on every signed semver tag.
6
+
7
+ ---
8
+
9
+ ## 1. Goals
10
+
11
+ Create a single, auditable GitHub Actions workflow that:
12
+
13
+ 1. Triggers on semver tag push (`v*.*.*` and `v*.*.*-*`).
14
+ 2. Refuses to publish unless **both** the commit the tag points to **and** the tag object itself are cryptographically signed and verified by GitHub.
15
+ 3. Runs the full quality gate (lint, typecheck, tests, build) on a Node version matrix (20, 22, 24) before any publish action.
16
+ 4. Publishes to the public npm registry with Sigstore provenance.
17
+ 5. Automatically derives the npm `dist-tag` from the tag suffix (stable → `latest`, prerelease → `beta`/`rc`/`alpha`/etc).
18
+ 6. Creates a GitHub Release with the built tarball attached and auto-generated changelog, marked as `prerelease` when appropriate.
19
+ 7. Fails hard at the earliest failing step — no partial state (no published version without a release, no release without a successful publish).
20
+
21
+ ## 2. Non-goals
22
+
23
+ - PR CI workflow (only release-on-tag is in scope).
24
+ - `workflow_dispatch` with `dry_run` flag (deferred, YAGNI).
25
+ - Integration tests hitting a live API (no such tests exist today).
26
+ - Publishing to GitHub Packages or any registry other than public npm.
27
+ - Automatic version bumping, changelog generation outside `gh release --generate-notes`, or release-please style automation.
28
+ - Unpublish / rollback automation (stays manual).
29
+
30
+ ## 3. Trigger
31
+
32
+ ```yaml
33
+ on:
34
+ push:
35
+ tags:
36
+ - 'v[0-9]+.[0-9]+.[0-9]+'
37
+ - 'v[0-9]+.[0-9]+.[0-9]+-*'
38
+ ```
39
+
40
+ Two explicit glob patterns. GitHub filters these *before* scheduling runners, so malformed tags like `v-test` or `release-1` never spend CI minutes. The first pattern matches stable releases (`v1.2.3`); the second matches prereleases (`v1.2.3-beta.1`, `v1.2.3-rc.2`, `v2.0.0-alpha.0`, etc).
41
+
42
+ ## 4. Workflow-level permissions and concurrency
43
+
44
+ ```yaml
45
+ permissions:
46
+ contents: write # github-release job creates releases
47
+ id-token: write # publish-npm job generates OIDC token for --provenance
48
+
49
+ concurrency:
50
+ group: release-${{ github.ref }}
51
+ cancel-in-progress: false
52
+ ```
53
+
54
+ **Permissions** are declared at the workflow level (not per job) because both are needed and `contents: read` alone is insufficient for the release step. No other permissions granted — principle of least privilege.
55
+
56
+ **Concurrency** groups runs by tag ref with `cancel-in-progress: false`. Same tag cannot run twice in parallel (defense against accidental double-push), but distinct tags run independently. `cancel-in-progress` is explicitly `false` because a publish-in-flight must never be cancelled mid-flight (would leave npm and GitHub in inconsistent states).
57
+
58
+ ## 5. Job graph
59
+
60
+ Four jobs, chained strictly via `needs:`. Any failure aborts the entire chain.
61
+
62
+ ```
63
+ verify-signature → test (matrix 20/22/24) → publish-npm → github-release
64
+ ```
65
+
66
+ ### 5.1 Job: `verify-signature`
67
+
68
+ **Runs-on:** `ubuntu-24.04`
69
+ **Responsibility:** Refuse to proceed unless both the commit and the annotated tag object are verified.
70
+
71
+ **Algorithm:**
72
+
73
+ 1. **Commit check**
74
+ - `GET /repos/{owner}/{repo}/commits/{github.sha}`
75
+ - Read `.commit.verification.verified`
76
+ - If not `true`: log `.commit.verification.reason` (examples: `unsigned`, `unknown_key`, `bad_email`, `unverified_email`, `not_signing_key`, `expired_key`) and `exit 1`.
77
+
78
+ 2. **Resolve tag object**
79
+ - `GET /repos/{owner}/{repo}/git/refs/tags/{github.ref_name}`
80
+ - Read `.object.type`
81
+ - If `"commit"`: the tag is a lightweight tag (no signature). Emit error telling the user to use `git tag -s` and `exit 1`.
82
+ - If `"tag"`: read `.object.sha` (the SHA of the tag object itself).
83
+
84
+ 3. **Tag signature check**
85
+ - `GET /repos/{owner}/{repo}/git/tags/{tag_object_sha}`
86
+ - Read `.verification.verified`
87
+ - If not `true`: log `.verification.reason` and `exit 1`.
88
+
89
+ **Implementation tool:** `gh api` (pre-installed on GitHub-hosted runners, authenticated via `GH_TOKEN=${{ github.token }}`). JSON parsing via `--jq`.
90
+
91
+ **Error output:** uses `::error::` workflow command so failures surface as annotations on the tag's commit in the Actions UI.
92
+
93
+ **Why both checks?**
94
+ - Commit-only: misses a lightweight tag created on top of a verified commit — you'd publish without ever signing the release itself.
95
+ - Tag-only: misses the case where the tag is signed but the underlying commit came from an unverified push.
96
+ - Both: guarantees a complete cryptographic chain from commit → tag → release.
97
+
98
+ ### 5.2 Job: `test`
99
+
100
+ **Runs-on:** `ubuntu-24.04`
101
+ **Needs:** `verify-signature`
102
+ **Strategy:** Matrix on `node-version: [20, 22, 24]`, `fail-fast: true`.
103
+
104
+ **Steps:**
105
+
106
+ 1. `actions/checkout@v5`
107
+ 2. `actions/setup-node@v5` with `node-version: ${{ matrix.node-version }}` and `cache: 'npm'`
108
+ 3. `npm ci`
109
+ 4. `npm run lint`
110
+ 5. `npm run typecheck`
111
+ 6. `npm run test` (vitest)
112
+ 7. `npm run build`
113
+
114
+ All three matrix cells must succeed for `needs: test` downstream to unblock.
115
+
116
+ **Rationale for matrix:** `package.json` declares `engines.node: ">=20"`. The release must prove the SDK works on every LTS-or-current supported runtime before going to npm. Runtime is cheap (three parallel jobs) and it catches runtime-specific regressions that only manifest on newer Node versions (e.g., changes to `fetch`, `URL`, or `AbortController` semantics).
117
+
118
+ ### 5.3 Job: `publish-npm`
119
+
120
+ **Runs-on:** `ubuntu-24.04`
121
+ **Needs:** `test`
122
+ **Outputs:** `is_prerelease`, `disttag`, `version` (consumed by `github-release`).
123
+
124
+ **Steps:**
125
+
126
+ 1. `actions/checkout@v5`
127
+ 2. `actions/setup-node@v5` with `node-version: 22`, `registry-url: 'https://registry.npmjs.org'`, `cache: 'npm'`
128
+ 3. `npm ci`
129
+ 4. `npm run build`
130
+ 5. **Compute dist-tag and version (step id: `disttag`)**
131
+
132
+ ```bash
133
+ set -euo pipefail
134
+ tag="${GITHUB_REF_NAME}" # e.g. "v1.2.3-beta.1"
135
+ version="${tag#v}" # strip leading v → "1.2.3-beta.1"
136
+ if [[ "$version" == *-* ]]; then
137
+ suffix="${version#*-}" # "beta.1"
138
+ disttag="${suffix%%.*}" # "beta"
139
+ is_prerelease=true
140
+ else
141
+ disttag="latest"
142
+ is_prerelease=false
143
+ fi
144
+ echo "version=$version" >> "$GITHUB_OUTPUT"
145
+ echo "disttag=$disttag" >> "$GITHUB_OUTPUT"
146
+ echo "is_prerelease=$is_prerelease" >> "$GITHUB_OUTPUT"
147
+ ```
148
+
149
+ 6. **Version mismatch guard**
150
+
151
+ ```bash
152
+ pkg_version=$(node -p "require('./package.json').version")
153
+ if [ "$pkg_version" != "${{ steps.disttag.outputs.version }}" ]; then
154
+ echo "::error::package.json version ($pkg_version) does not match git tag (${{ steps.disttag.outputs.version }})"
155
+ exit 1
156
+ fi
157
+ ```
158
+
159
+ 7. **Publish**
160
+
161
+ ```bash
162
+ npm publish --provenance --access public --tag "${{ steps.disttag.outputs.disttag }}"
163
+ ```
164
+
165
+ With `env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}`.
166
+
167
+ **Tag-to-dist-tag mapping (reference):**
168
+
169
+ | Tag pushed | `version` | `disttag` | `is_prerelease` |
170
+ |-----------------------|-------------------|-----------|-----------------|
171
+ | `v1.2.3` | `1.2.3` | `latest` | `false` |
172
+ | `v1.2.3-beta.1` | `1.2.3-beta.1` | `beta` | `true` |
173
+ | `v1.2.3-beta.4` | `1.2.3-beta.4` | `beta` | `true` |
174
+ | `v1.2.3-rc.1` | `1.2.3-rc.1` | `rc` | `true` |
175
+ | `v2.0.0-alpha.7` | `2.0.0-alpha.7` | `alpha` | `true` |
176
+ | `v2.0.0-next.0` | `2.0.0-next.0` | `next` | `true` |
177
+
178
+ **Why `--access public`?** `@intelmesh/sdk` is a scoped package; npm defaults scoped packages to private. The flag makes it explicit and prevents accidental private publishes.
179
+
180
+ **Why `--provenance`?** Generates a Sigstore attestation linking the published tarball to the GitHub Actions run that built it. Consumers see a "provenance" badge on `npmjs.com/package/@intelmesh/sdk`. Requires `id-token: write` permission (already declared) and the npm account to have the package linked to this repo (via `package.json` `repository` field — must be verified present during implementation).
181
+
182
+ **Why use the tag as the source of truth, not `package.json`?** If the developer tags `v1.2.3-beta.1` but forgets to bump `package.json` from `1.2.3`, we want a loud failure, not silent guessing. The version mismatch guard enforces this.
183
+
184
+ ### 5.4 Job: `github-release`
185
+
186
+ **Runs-on:** `ubuntu-24.04`
187
+ **Needs:** `publish-npm`
188
+
189
+ **Steps:**
190
+
191
+ 1. `actions/checkout@v5` with `fetch-depth: 0` (needed for `gh release --generate-notes` to compute since-last-tag)
192
+ 2. `actions/setup-node@v5` with `node-version: 22`, `cache: 'npm'`
193
+ 3. `npm ci`
194
+ 4. `npm run build`
195
+ 5. `npm pack` (produces `intelmesh-sdk-<version>.tgz`)
196
+ 6. **Create release** — split into two steps, gated by `if:` on the `is_prerelease` output, to keep each command readable:
197
+
198
+ ```yaml
199
+ - name: Create GitHub Release (stable)
200
+ if: needs.publish-npm.outputs.is_prerelease == 'false'
201
+ env:
202
+ GH_TOKEN: ${{ github.token }}
203
+ run: |
204
+ gh release create "$GITHUB_REF_NAME" \
205
+ ./intelmesh-sdk-*.tgz \
206
+ --title "$GITHUB_REF_NAME" \
207
+ --generate-notes
208
+
209
+ - name: Create GitHub Release (prerelease)
210
+ if: needs.publish-npm.outputs.is_prerelease == 'true'
211
+ env:
212
+ GH_TOKEN: ${{ github.token }}
213
+ run: |
214
+ gh release create "$GITHUB_REF_NAME" \
215
+ ./intelmesh-sdk-*.tgz \
216
+ --title "$GITHUB_REF_NAME" \
217
+ --generate-notes \
218
+ --prerelease
219
+ ```
220
+
221
+ Two mutually exclusive steps are clearer than one step with a dynamic flag inlined via GHA expression syntax.
222
+
223
+ **Why rebuild here instead of reusing `publish-npm` artifacts?** GitHub Actions jobs have isolated file systems. The alternatives are (a) `actions/upload-artifact` + `actions/download-artifact` between jobs, or (b) rebuild. Rebuild is simpler, deterministic, and the build is fast (<30s for this SDK). If build time grows, switch to upload/download. YAGNI for now.
224
+
225
+ **Why `--generate-notes`?** GitHub auto-generates release notes from commits and PRs since the previous tag. No need to maintain a manual CHANGELOG for the MVP — the git log and PR titles are the source of truth. Manual CHANGELOG can be added later if needed.
226
+
227
+ **Why create release *after* npm publish (not before)?** If npm publish fails, we don't want a dangling GitHub release pointing at a version that isn't in the registry. Publish first, release second.
228
+
229
+ ## 6. Secrets
230
+
231
+ | Secret | Source | Used in | Required |
232
+ |----------------|---------------------------------|---------------------------------|----------|
233
+ | `NPM_TOKEN` | Repo Settings → Secrets → Actions | `publish-npm` | Yes |
234
+ | `GITHUB_TOKEN` | Automatic (injected) | `verify-signature`, `github-release` | Auto |
235
+
236
+ **`NPM_TOKEN` provisioning (one-time, manual):**
237
+
238
+ 1. On `npmjs.com`, log in with the owner account for `@intelmesh`.
239
+ 2. Settings → Access Tokens → Generate New Token → Granular Access Token.
240
+ 3. Scope: `Read and write` on packages under `@intelmesh`.
241
+ 4. Expiration: 1 year (document renewal in team runbook).
242
+ 5. Copy the token.
243
+ 6. In the GitHub repo: Settings → Secrets and variables → Actions → New repository secret, name `NPM_TOKEN`.
244
+
245
+ **Provenance prerequisite check:** During implementation, verify `package.json` has a `repository` field pointing to `github.com/intelmesh/intelmesh-sdk-ts`. If missing, add it — `npm publish --provenance` uses it to link the package to the source repo.
246
+
247
+ ## 7. Failure modes
248
+
249
+ | Scenario | Failing job | User-visible message |
250
+ |-----------------------------------------|-------------------|---------------------------------------------------------------------------------------|
251
+ | Push of lightweight tag | `verify-signature` | `Tag vX.Y.Z is a lightweight tag. Use 'git tag -s' to create a signed annotated tag.` |
252
+ | Commit not signed | `verify-signature` | `Commit <sha> is NOT verified. Reason: unsigned` |
253
+ | Signing key not registered on GitHub | `verify-signature` | `Reason: unknown_key` |
254
+ | Expired signing key | `verify-signature` | `Reason: expired_key` |
255
+ | Signer email not verified on GitHub | `verify-signature` | `Reason: unverified_email` |
256
+ | Lint / typecheck / test / build failure | `test` | ESLint / tsc / vitest annotations on the run |
257
+ | `package.json` version ≠ tag | `publish-npm` | `package.json version (X) does not match git tag (Y)` |
258
+ | Version already published | `publish-npm` | npm 403; step fails; no release created |
259
+ | `NPM_TOKEN` missing/expired | `publish-npm` | npm 401 |
260
+ | Provenance OIDC fails | `publish-npm` | npm publish error mentioning `id-token` |
261
+
262
+ All failures propagate via `needs:` so no downstream job runs. There is no partial state: a failed pipeline leaves the npm registry and GitHub Releases untouched by downstream jobs.
263
+
264
+ ## 8. Re-running a failed release
265
+
266
+ **Supported:** GitHub Actions "Re-run failed jobs" works because the trigger is `push: tags` and the tag continues to point to the same commit. Use this for flaky failures (npm registry 502, runner network blip, etc).
267
+
268
+ **Not supported by the workflow (manual process):**
269
+
270
+ - Unpublishing a broken release from npm: `npm unpublish @intelmesh/sdk@X.Y.Z` within the 72-hour window, or contact npm support after.
271
+ - Removing a release from GitHub: `gh release delete vX.Y.Z`.
272
+ - Deleting/moving a tag: `git tag -d vX.Y.Z && git push --delete origin vX.Y.Z && git tag -s vX.Y.Z <new-sha> && git push origin vX.Y.Z`.
273
+
274
+ These stay manual because they are operational rollbacks, not part of a normal release flow, and automating them would be footgun territory.
275
+
276
+ ## 9. Testing the workflow itself
277
+
278
+ Before the first real release, validate the pipeline end-to-end on a throwaway version:
279
+
280
+ 1. Create a branch with a signed commit.
281
+ 2. Bump `package.json` to a harmless prerelease version like `0.1.0-alpha.0`.
282
+ 3. Sign-tag: `git tag -s v0.1.0-alpha.0 -m "pipeline smoke test"`.
283
+ 4. Push tag: `git push origin v0.1.0-alpha.0`.
284
+ 5. Watch the Actions run:
285
+ - `verify-signature` must pass (if not: fix GPG/SSH setup first).
286
+ - `test` must pass on all three Node versions.
287
+ - `publish-npm` will actually publish to npm under dist-tag `alpha`. This is fine — `alpha` dist-tag doesn't affect default installs.
288
+ - `github-release` creates a prerelease on GitHub.
289
+ 6. After success: `npm dist-tag rm @intelmesh/sdk alpha` (cleanup, optional) and delete the GitHub release if desired.
290
+
291
+ Alternatively, if contaminating the npm registry with a test version is undesirable, a future `workflow_dispatch` with `dry_run` input can be added (explicitly deferred — see Non-goals).
292
+
293
+ ## 10. File layout
294
+
295
+ ```
296
+ .github/
297
+ workflows/
298
+ release.yml ← this design's output
299
+ ```
300
+
301
+ Single file. All four jobs live in one workflow so the full release pipeline is visible in one place.
302
+
303
+ ## 11. Open implementation questions
304
+
305
+ None at spec time. Items to verify during implementation:
306
+
307
+ - `package.json` `repository` field is present and points to the correct GitHub URL (required for `--provenance`).
308
+ - The npm account owning `@intelmesh` exists and `NPM_TOKEN` has been provisioned before merging the workflow (otherwise the first tag push will fail at `publish-npm`).
309
+ - GitHub org `intelmesh` has `Actions` enabled with `write` permissions for workflow tokens (Settings → Actions → Workflow permissions).
@@ -0,0 +1,38 @@
1
+ import js from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import security from 'eslint-plugin-security';
4
+ import jsdoc from 'eslint-plugin-jsdoc';
5
+
6
+ export default tseslint.config(
7
+ js.configs.recommended,
8
+ ...tseslint.configs.strictTypeChecked,
9
+ security.configs.recommended,
10
+ jsdoc.configs['flat/recommended-typescript'],
11
+ {
12
+ languageOptions: {
13
+ parserOptions: {
14
+ project: './tsconfig.eslint.json',
15
+ tsconfigRootDir: import.meta.dirname,
16
+ },
17
+ },
18
+ rules: {
19
+ '@typescript-eslint/no-explicit-any': 'error',
20
+ '@typescript-eslint/explicit-function-return-type': 'error',
21
+ '@typescript-eslint/no-unused-vars': [
22
+ 'error',
23
+ { argsIgnorePattern: '^_' },
24
+ ],
25
+ 'complexity': ['error', 10],
26
+ 'max-lines-per-function': ['error', { max: 50, skipBlankLines: true, skipComments: true }],
27
+ 'jsdoc/require-jsdoc': [
28
+ 'error',
29
+ { publicOnly: true },
30
+ ],
31
+ 'jsdoc/require-description': 'error',
32
+ 'no-console': 'error',
33
+ },
34
+ },
35
+ {
36
+ ignores: ['dist/', 'node_modules/', 'coverage/', '*.config.*', '*.cjs'],
37
+ },
38
+ );
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@intelmesh/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Official Node.js client for the IntelMesh Risk Intelligence Engine API",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "intelmesh": {
18
+ "apiVersion": "v1",
19
+ "minServerVersion": "1.0.0"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format esm --dts --clean",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "lint": "eslint src/ tests/",
26
+ "lint:fix": "eslint src/ tests/ --fix",
27
+ "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
28
+ "generate": "openapi-typescript ../docs/swagger.json -o src/generated/types.ts && prettier --write src/generated/types.ts",
29
+ "typecheck": "tsc --noEmit",
30
+ "prepare": "husky"
31
+ },
32
+ "lint-staged": {
33
+ "*.ts": [
34
+ "eslint --fix",
35
+ "prettier --write"
36
+ ]
37
+ },
38
+ "keywords": [
39
+ "intelmesh",
40
+ "risk",
41
+ "fraud",
42
+ "rules-engine",
43
+ "cel"
44
+ ],
45
+ "author": "IntelMesh",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/intelmesh/intelmesh-sdk-ts.git"
50
+ },
51
+ "homepage": "https://github.com/intelmesh/intelmesh-sdk-ts#readme",
52
+ "bugs": {
53
+ "url": "https://github.com/intelmesh/intelmesh-sdk-ts/issues"
54
+ },
55
+ "devDependencies": {
56
+ "@commitlint/cli": "^20.5.0",
57
+ "@commitlint/config-conventional": "^20.5.0",
58
+ "@eslint/js": "^10.0.1",
59
+ "@types/node": "^25.5.2",
60
+ "eslint": "^10.2.0",
61
+ "eslint-plugin-jsdoc": "^62.9.0",
62
+ "eslint-plugin-security": "^4.0.0",
63
+ "husky": "^9.1.7",
64
+ "lint-staged": "^16.4.0",
65
+ "openapi-typescript": "^7.13.0",
66
+ "prettier": "^3.8.1",
67
+ "tsup": "^8.5.1",
68
+ "typescript": "^6.0.2",
69
+ "typescript-eslint": "^8.58.1",
70
+ "vitest": "^4.1.3"
71
+ }
72
+ }
@@ -0,0 +1,72 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Fluent EventBuilder
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { IngestRequest } from '../types.js';
6
+
7
+ /**
8
+ * Fluent builder for constructing IngestRequest payloads.
9
+ */
10
+ export class EventBuilder {
11
+ private eventType = '';
12
+ private idempotencyKey: string | undefined;
13
+ private payload: Record<string, unknown> = {};
14
+
15
+ /**
16
+ * Sets the event type.
17
+ * @param type - The event type string (e.g. "transaction.pix").
18
+ * @returns This builder for chaining.
19
+ */
20
+ type(type: string): this {
21
+ this.eventType = type;
22
+ return this;
23
+ }
24
+
25
+ /**
26
+ * Sets the idempotency key for deduplication.
27
+ * @param key - The idempotency key.
28
+ * @returns This builder for chaining.
29
+ */
30
+ idempotency(key: string): this {
31
+ this.idempotencyKey = key;
32
+ return this;
33
+ }
34
+
35
+ /**
36
+ * Sets a single payload field.
37
+ * @param key - The field name.
38
+ * @param value - The field value.
39
+ * @returns This builder for chaining.
40
+ */
41
+ set(key: string, value: unknown): this {
42
+ Object.assign(this.payload, { [key]: value });
43
+ return this;
44
+ }
45
+
46
+ /**
47
+ * Merges multiple fields into the payload.
48
+ * @param data - Record of fields to merge.
49
+ * @returns This builder for chaining.
50
+ */
51
+ data(data: Record<string, unknown>): this {
52
+ Object.assign(this.payload, data);
53
+ return this;
54
+ }
55
+
56
+ /**
57
+ * Builds and returns the IngestRequest.
58
+ * @returns The constructed IngestRequest.
59
+ * @throws {Error} If event type is not set.
60
+ */
61
+ build(): IngestRequest {
62
+ if (!this.eventType) {
63
+ throw new Error('EventBuilder: event type is required');
64
+ }
65
+
66
+ return {
67
+ event_type: this.eventType,
68
+ payload: { ...this.payload },
69
+ ...(this.idempotencyKey !== undefined && { idempotency_key: this.idempotencyKey }),
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,143 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Fluent RuleBuilder
3
+ // ---------------------------------------------------------------------------
4
+
5
+ import type { Actions, CreateRuleRequest, Decision, Flow, Severity } from '../types.js';
6
+
7
+ /**
8
+ * Fluent builder for constructing CreateRuleRequest payloads.
9
+ */
10
+ export class RuleBuilder {
11
+ private ruleName = '';
12
+ private ruleExpression = '';
13
+ private ruleApplicableWhen = '';
14
+ private rulePhaseId = '';
15
+ private rulePriority = 0;
16
+ private ruleEnabled = true;
17
+ private ruleDryRun = false;
18
+ private ruleActions: Actions = {};
19
+
20
+ /**
21
+ * Sets the rule name.
22
+ * @param n - The rule name.
23
+ * @returns This builder for chaining.
24
+ */
25
+ name(n: string): this {
26
+ this.ruleName = n;
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Sets the CEL expression for the rule.
32
+ * @param expr - The CEL expression.
33
+ * @returns This builder for chaining.
34
+ */
35
+ expression(expr: string): this {
36
+ this.ruleExpression = expr;
37
+ return this;
38
+ }
39
+
40
+ /**
41
+ * Sets the condition under which this rule applies.
42
+ * @param condition - The applicable-when CEL expression.
43
+ * @returns This builder for chaining.
44
+ */
45
+ applicableWhen(condition: string): this {
46
+ this.ruleApplicableWhen = condition;
47
+ return this;
48
+ }
49
+
50
+ /**
51
+ * Sets the phase this rule belongs to.
52
+ * @param id - The phase ID.
53
+ * @returns This builder for chaining.
54
+ */
55
+ phase(id: string): this {
56
+ this.rulePhaseId = id;
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Sets the rule priority (lower number = higher priority).
62
+ * @param p - The priority value.
63
+ * @returns This builder for chaining.
64
+ */
65
+ priority(p: number): this {
66
+ this.rulePriority = p;
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Sets whether the rule is enabled.
72
+ * @param value - True to enable, false to disable.
73
+ * @returns This builder for chaining.
74
+ */
75
+ enabled(value: boolean): this {
76
+ this.ruleEnabled = value;
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * Sets whether the rule runs in dry-run mode.
82
+ * @param value - True for dry-run.
83
+ * @returns This builder for chaining.
84
+ */
85
+ dryRun(value: boolean): this {
86
+ this.ruleDryRun = value;
87
+ return this;
88
+ }
89
+
90
+ /**
91
+ * Sets the decision action when the rule matches.
92
+ * @param action - The action string.
93
+ * @param severity - The severity level.
94
+ * @returns This builder for chaining.
95
+ */
96
+ decide(action: string, severity: Severity): this {
97
+ const decision: Decision = { action, severity };
98
+ this.ruleActions = { ...this.ruleActions, decision };
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * Sets the flow control when the rule matches.
104
+ * @param flow - The flow control value.
105
+ * @returns This builder for chaining.
106
+ */
107
+ flow(flow: Flow): this {
108
+ this.ruleActions = { ...this.ruleActions, flow };
109
+ return this;
110
+ }
111
+
112
+ /**
113
+ * Sets the score delta when the rule matches.
114
+ * @param delta - The score points to add.
115
+ * @returns This builder for chaining.
116
+ */
117
+ scoreDelta(delta: number): this {
118
+ this.ruleActions = { ...this.ruleActions, score: { add: delta } };
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Builds and returns the CreateRuleRequest.
124
+ * @returns The constructed CreateRuleRequest.
125
+ * @throws {Error} If required fields are missing.
126
+ */
127
+ build(): CreateRuleRequest {
128
+ if (!this.ruleName) throw new Error('RuleBuilder: name is required');
129
+ if (!this.ruleExpression) throw new Error('RuleBuilder: expression is required');
130
+ if (!this.rulePhaseId) throw new Error('RuleBuilder: phase is required');
131
+
132
+ return {
133
+ name: this.ruleName,
134
+ expression: this.ruleExpression,
135
+ applicable_when: this.ruleApplicableWhen,
136
+ phase_id: this.rulePhaseId,
137
+ priority: this.rulePriority,
138
+ enabled: this.ruleEnabled,
139
+ dry_run: this.ruleDryRun,
140
+ actions: this.ruleActions,
141
+ };
142
+ }
143
+ }