@websandhq/contracts
v1.1.0
Published
JSON Schema contracts for the Websand V4 data ingestion API. Used by external integrations (Shopify marketing app, Zapier, future partners) to validate that their outgoing payloads will be accepted by Websand before the request is sent.
Readme
@websandhq/contracts
JSON Schema contracts for the Websand V4 data ingestion API. Used by external integrations (Shopify marketing app, Zapier, future partners) to validate that their outgoing payloads will be accepted by Websand before the request is sent.
The schemas are descriptive — they match what the platform's JSONata
preprocessing currently accepts, not a stricter ideal. When the ingestion
behavior changes, schemas change with it; consumers track a caret range
(e.g. "@websandhq/contracts": "^1.2.0") so patches and minors flow in
automatically through Dependabot, and majors are opened as separate PRs
for deliberate review. CI gates the merge in both cases — see the
consumer runbook.
Exports
| Schema | Endpoint | Purpose |
|---|---|---|
| shopifyTransactionInputSchema | POST /data/transaction | Raw payload shape for the Shopify transaction resource (pre-JSONata) |
| subscriberInputSchema | POST /data/subscriber | Raw payload shape for the subscriber resource (pre-JSONata) |
| directInputSchema | POST /data/:directType | Canonical profile event shape for clients that skip preprocessing |
All three are plain JSON Schema constants (as const literals). The package
has zero runtime dependencies — bring your own validator (AJV recommended,
to match Websand's own validation).
Publishing Runbook
Published to public npm at https://www.npmjs.com/package/@websandhq/contracts.
Consumers install with a plain pnpm add -D @websandhq/contracts — no
registry override, no auth token.
How publishing happens
The publish-contracts.yml
workflow triggers on every push to main that touches packages/contracts/**.
The workflow:
- Installs the workspace and builds
@websandhq/contracts. - Reads
versionfrompackages/contracts/package.json. - Queries the npm registry — if that version already exists, the publish step is skipped (re-runs on the same commit are safe; no-op).
- Otherwise publishes via
pnpm publish --no-git-checks --provenance --access=public.
The --provenance flag attaches a sigstore attestation that the package was
built in this exact GitHub Actions workflow from this exact commit. It shows
up as a "Verified" badge on the npm package page and lets consumers verify
the supply chain — free for public packages, requires id-token: write
permission (already declared in the workflow).
Auth: uses an NPM_TOKEN repo secret (npm automation token, not a PAT). See
"First-time setup" below — this is the only credential to manage.
First-time setup (already done — kept here for disaster recovery)
If the NPM_TOKEN secret is ever lost, rotated, or this needs to be
re-bootstrapped from scratch:
npm org membership. The
@websandhqscope on npmjs.com must exist and the publishing identity must have publish access. Confirm at https://www.npmjs.com/settings/websandhq/members. If the org doesn't exist, create it at https://www.npmjs.com/org/create (free for public packages).Provenance prerequisite. For the
--provenanceflag to attach a sigstore attestation, the source repo must be public on GitHub ANDpackages/contracts/package.jsonrepository.urlmust match the canonical GitHub URL (it currently does:https://github.com/WebsandHQ/websand-v4.git). Don't rename or move the repo without updating both.Create an automation token. npmjs.com → profile → Access Tokens → Generate New Token → Granular Access Token. Configure:
- Packages and scopes permission: write access to
@websandhq/*scope only. - Bypass Two-Factor Authentication: ✅ check this. The publishing
identity has 2FA enforced (it should — npm requires it for
publishers); without this checkbox the workflow will fail with
EOTP(one-time password required) or hang on the publish step. Granular tokens with this flag set are the documented npm path for CI publishing. - Expiration: set to the maximum the UI allows. As of late 2025, npm caps write-enabled granular tokens at significantly less than a calendar year (the exact cap moves; the UI shows the current limit when you select the token scope). Set a calendar reminder for rotation at the cap shown in the UI, not a year out.
- Packages and scopes permission: write access to
Store the token in GitHub.
websand-v4repo → Settings → Secrets and variables → Actions → New repository secret. Name:NPM_TOKEN. Paste the token value. Save.Verify with a no-op publish. Bump the contracts patch version, merge a PR, watch the workflow log for the "Publish to npmjs.com" step succeeding. Note: the
npm viewstep before it is an anonymous public-registry query — it does NOT validateNPM_TOKEN. A bad, missing, or scope-wrong token will only surface at the publish step itself withE401/E403. Watch for that step's exit code, not thenpm viewstep's.
Releasing a new version
The version bump is manual and deliberate — the workflow does not bump automatically. This forces a conscious decision about semver impact every time schemas change.
Make the schema change in
packages/contracts/src/.Run the chain tests — these prove the schema still matches the live ingestion path and that
directInputSchemastays in lockstep with the canonicalingestionProfileEventJsonSchemafrom@repo/shared:cd apps/server && pnpm exec vitest run src/scripts/platform-endpoint-resources.test.mtsAll three describe blocks must pass: input→preprocess→resource→canonical, negative cases with explicit error-path assertions, and the parity matrix. If parity fails, the standalone copy in
direct-input.mtshas drifted from the SSOT — sync it before bumping the version.Bump the version in
packages/contracts/package.jsonfollowing semver:| Change | Bump | Examples | |---|---|---| | New optional field, new required-on-output field that JSONata fills in, doc-only edits | patch | adding a new
properties.countryentry; documenting an existing field | | New endpoint schema export, new oneOf branch indirectInputSchema(additive) | minor | adding a new marketing event name; exporting a new*InputSchema| | Tightened type or format on an existing field, removed/renamed field, narrowedrequiredsemantics | major | tighteningorderdatefromstringtostring+format: "date-time"; removing a deprecated field |Run from inside
packages/contracts/:pnpm version <patch|minor|major> --no-git-tag-versionThe
--no-git-tag-versionflag is required. Without it,pnpm versioncreates a git commit AND a tag pointing at HEAD on your local branch. That tag is on the PR branch, not onmain— by the time the PR merges, the tag points at an orphan commit and pollutes the tag namespace. The publish workflow keys off the manifest version, not git tags, so tags add no value and create cleanup work. If you prefer, edit theversionfield inpackage.jsondirectly — same effect, no flag to remember.Update the Schema changelog section (below in this README) if it's a major or minor bump. Patch bumps don't need a changelog entry.
Commit, PR, merge. The workflow runs on merge to
mainand publishes to npmjs.com within ~2 minutes.Verify the new version is live:
# any machine — public registry, no auth needed npm view @websandhq/contracts@<new-version> versionOr open https://www.npmjs.com/package/@websandhq/contracts and check the versions list. The "Provenance" badge should be present on every version published via this workflow.
Common publish failures
| Symptom | Cause | Fix |
|---|---|---|
| Workflow log: npm error code E401 or Unable to authenticate | NPM_TOKEN secret is missing, expired, or scoped wrong | Regenerate per "First-time setup" step 2. Granular tokens expire — set a rotation reminder. |
| Workflow log: npm error code EOTP or step hangs waiting for one-time password | The granular token was created without "Bypass Two-Factor Authentication" checked | Regenerate the token with that checkbox set; update the NPM_TOKEN secret in repo settings. |
| Workflow log: npm error code E403 — You must be logged in to publish packages | Token lacks publish access for @websandhq/* | Granular token's package-permission scope is wrong. Recreate with write access to the scope. |
| Workflow log: npm error code EPUBLISHCONFLICT | A version equal to or older than package.json is already published — and the npm view guard somehow didn't catch it (e.g. registry replication lag) | Bump to the next patch and re-merge. The skip-if-exists guard is best-effort, not transactional. |
| Workflow log: npm error code EPROVENANCE or "provenance attestation failed" | id-token: write permission missing, or workflow not eligible (e.g. running on a fork PR) | Confirm the permissions: block declares id-token: write. Provenance only works on push to the canonical repo, not on forked PRs (which is fine — the workflow only runs on push to main). |
| Publish step skipped but you bumped the version | The version in package.json already matches a published version on npm | Re-bump the version (you likely cherry-picked an old commit, or someone else published manually). Each merge to main must bring a strictly-greater version, or it no-ops. |
| Type errors at consumer install (Cannot find module '@websandhq/contracts/dist/index.d.mts') | Build step didn't run, or files: ["dist"] was edited | Check the workflow's build step logs. The published tarball must contain dist/ — verify with cd packages/contracts && npm pack --dry-run locally. |
Rolling back a bad publish
Don't npm unpublish. Two reasons, both worse than they sound:
- The version number is permanently burned. Per npm policy
(docs),
package-name@versionis unique and cannot be reused by unpublishing and re-publishing it. So if you unpublish1.4.0planning to fix the bug and re-issue1.4.0, you can't —1.4.0is gone forever and any fix has to ship as1.4.1or higher. This is the same end state as fix-forward, but with extra steps and a confusing version-history hole. - Reproducible builds break for cached installs. Anyone whose
pnpm installalready pulled the bad version still has it locked in their lockfile; their builds keep working off the cache, so they don't know to upgrade until something else triggers a re-resolve. Then they suddenly hit404on the version their lockfile points at.
The only valid uses of unpublish are accidental publishes of secrets
(which can't happen here — pure JSON Schema, no environment config) and
DMCA takedowns. If you unpublish all versions of the package, npm
additionally blocks re-creating the package name for 24 hours — separate
policy from the per-version burn above.
Instead, publish a fix-forward version:
Bump again (e.g.
1.4.1over a broken1.4.0) with the schema fix.Open a PR in
websand-email-marketing-v2bumping the dependency past the broken version. (Today that's the only consumer; future consumers discover the fix via Dependabot.)Optional: deprecate the bad version so consumers see a warning at install time:
npm deprecate @websandhq/[email protected] "Bad release — use 1.4.1+"Run from any machine logged in as a publisher of the scope. Deprecation doesn't remove the version (so cached installs keep working) but warns anyone resolving it fresh.
Schema changelog
Document any minor or major bump here. Patch-only changes don't need a row.
| Version | Date | Change | Migration |
|---|---|---|---|
| 1.1.0 | 2026-05-15 | shopifyTransactionInputSchema: orderId and total.total now declare type: ["string", "number"] instead of unconstrained {}. Catches null/boolean/array values at the consumer-side validator before they hit Websand. | None — JSONata still coerces both shapes on the platform side, so existing payloads with strings or numbers continue to pass. Payloads with null/boolean/array values were already rejected downstream; this surfaces the rejection earlier. |
| 1.0.0 | 2026-05-14 | Initial release. shopifyTransactionInputSchema, subscriberInputSchema, directInputSchema all match canonical ingestion behavior as of 00cbc151 (v2.6.0). | — |
How consumers use this
See the consumer-side runbook in the marketing repo:
websand-email-marketing-v2/docs/websand-contracts.md.
Short version: install with pnpm add -D @websandhq/contracts, validate the
output of your transform functions against the schemas with AJV in a test,
fail CI when transforms drift from the contract.
