Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/module-docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Module docs contract
on:
pull_request:
paths:
- tools/module-docs/**
- packages/v1-ready/hubspot/**
- .github/workflows/module-docs*.yml
- .github/workflows/release.yml
- package.json
workflow_dispatch:
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
- run: node --test tools/module-docs/*.test.cjs
- name: Pack the pilot with its real lifecycle hook
run: npm pack --workspace @friggframework/api-module-hubspot --pack-destination /tmp
- run: node tools/module-docs/check-pack.cjs /tmp
44 changes: 44 additions & 0 deletions .github/workflows/module-docs-delivery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Retry module documentation delivery
on:
workflow_dispatch:
inputs:
package:
description: Allowlisted published npm package
type: string
required: true
version:
description: Exact published version (no package publication)
type: string
required: true
permissions:
contents: read
jobs:
deliver:
if: vars.MODULE_DOCS_ENABLED == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: next
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/create-github-app-token@v2
id: app
with:
app-id: ${{ vars.MODULE_DOCS_APP_ID }}
private-key: ${{ secrets.MODULE_DOCS_APP_PRIVATE_KEY }}
owner: friggframework
repositories: frigg
- name: Verify package and retry dispatch
env:
MODULE_DOCS_TOKEN: ${{ steps.app.outputs.token }}
run: node tools/module-docs/deliver.cjs
- uses: actions/upload-artifact@v4
if: always()
with:
name: module-docs-delivery
path: .module-docs/delivery.json
include-hidden-files: true
if-no-files-found: ignore
46 changes: 45 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
release:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')"
if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, '[skip release]')"
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -21,6 +21,8 @@ jobs:
node-version: 18
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Check module documentation and examples
run: node --test tools/module-docs/*.test.cjs
- name: Auto release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
Expand All @@ -30,3 +32,45 @@ jobs:
run: |
npm ci
npx auto shipit
- name: Save published module identities for separate delivery
if: success() && hashFiles('.module-docs/published.json') != ''
uses: actions/upload-artifact@v4
with:
name: module-docs-published
path: .module-docs/published.json
include-hidden-files: true
retention-days: 30

module-docs:
needs: release
if: vars.MODULE_DOCS_ENABLED == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/download-artifact@v4
with:
name: module-docs-published
path: .module-docs
- uses: actions/create-github-app-token@v2
id: app
with:
app-id: ${{ vars.MODULE_DOCS_APP_ID }}
private-key: ${{ secrets.MODULE_DOCS_APP_PRIVATE_KEY }}
owner: friggframework
repositories: frigg
- name: Verify published packages and dispatch docs delivery
env:
MODULE_DOCS_TOKEN: ${{ steps.app.outputs.token }}
run: node tools/module-docs/deliver.cjs
- uses: actions/upload-artifact@v4
if: always()
with:
name: module-docs-delivery
path: .module-docs/delivery.json
include-hidden-files: true
if-no-files-found: ignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ yarn-error.log*
.env
.npmrc
/.nx/
/.module-docs/
**/module-docs.bundle.json
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "",
"private": true,
"scripts": {
"test:module-docs": "node --test tools/module-docs/*.test.cjs",
"auto:info": "auto info",
"test:all": "npm test --foreground-scripts --workspaces --if-present",
"test:api-module-managers": "jest --testMatch='<rootDir>/api-module-library/**/manager.test.js' --globalSetup='<rootDir>/scripts/set-up-tests.js' --globalTeardown='<rootDir>/scripts/tear-down-tests.js' --watch",
Expand Down Expand Up @@ -39,6 +40,7 @@
"versionBranches": true,
"onlyPublishWithReleaseLabel": true,
"plugins": [
"./tools/module-docs/auto-plugin.cjs",
"npm",
"all-contributors",
"conventional-commits",
Expand Down
6 changes: 6 additions & 0 deletions packages/v1-ready/hubspot/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env*
*.tgz
tests/
jest-*.js
jest.config.js
coverage/
27 changes: 27 additions & 0 deletions packages/v1-ready/hubspot/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# HubSpot

This module exports an OAuth-aware `Api`, a Frigg authorization `Definition`,
`Config`, and `extensions.webhooks`. It wraps selected CRM, list, association,
custom-object, property, owner, timeline, CMS and template operations. It is not
the complete HubSpot API or a generated replacement for the provider's SDK.

```bash
npm install {{PACKAGE}}@{{VERSION}}
```

This exact release declares Frigg compatibility `{{FRIGG_RANGE}}`. Its Node
engines range is `{{NODE_RANGE}}`; unknown means the package declares no range.
Declared compatibility is separate from tested compatibility. The documentation
pilot checks wrapper calls with fixtures; it does not establish a live-account
test or a matrix of Frigg versions. Use the runtime requirements of your installed
Frigg release.

- [First successful call](getting-started.md)
- [OAuth setup and credential lifecycle](authentication.md)
- [Public methods](reference.md)
- [Pagination, errors, webhooks and limitations](operations.md)
- [Release changes and support](changes.md)

The package uses the MIT license and is maintained by the Frigg Framework
maintainers. HubSpot product entitlements and scopes determine which operations
an account can use; exported method names do not grant those permissions.
44 changes: 44 additions & 0 deletions packages/v1-ready/hubspot/docs/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# OAuth setup and credential lifecycle

Create or configure a HubSpot OAuth app, register its redirect URI, and enable the
scopes required for the methods you will call. The first-call example requires
`crm.objects.contacts.read`; property discovery, other CRM objects, CMS features,
and webhooks can require different scopes or account subscriptions. Consult the
[HubSpot OAuth guide](https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/overview)
for the app generation you use.

Configure these values before importing the module:

| Variable | Used by the module |
|---|---|
| `HUBSPOT_CLIENT_ID` | OAuth client ID |
| `HUBSPOT_CLIENT_SECRET` | Code exchange, token introspection, and webhook signature verification |
| `HUBSPOT_SCOPE` | Space-separated scopes included in the authorization URL |
| `REDIRECT_URI` | Base callback URI; `Definition` appends `/hubspot` |

Register the complete callback URI, including `/hubspot`, in HubSpot. Avoid a
trailing slash on `REDIRECT_URI` that would create a double slash. The definition
reads environment values at module load time. The `Api` constructor can instead
receive `client_id`, `client_secret`, `scope`, `redirect_uri`, `state`,
`access_token`, and `refresh_token` through the Frigg OAuth2Requester.

`getAuthUri()` returns the constructed authorization URL. The application must
generate, persist and verify OAuth state and route the callback; this method does
not implement that callback. The definition exchanges the returned code through
the inherited `getTokenFromCode()` and persists `access_token` and `refresh_token`
on the credential.

This source sets the token endpoint to `/oauth/2026-03/token` and introspects tokens
through `/oauth/2026-03/token/introspect`. `getUserDetails()` combines
`/integrations/v1/me` with token introspection and therefore requires client
credentials as well as the token. It is not the minimal read-only first-call path.
See the [HubSpot OAuth endpoint migration](https://developers.hubspot.com/changelog/v1-oauth-api-deprecation)
for provider changes.

Refresh behavior is inherited from the installed Frigg OAuth2Requester. Use Frigg's
managed credential flow to persist refreshed credentials rather than discarding
them in an ephemeral `Api` instance. The module declares Frigg `{{FRIGG_RANGE}}`;
it does not independently promise a concurrency strategy or refresh schedule.
Reauthorize revoked or inactive grants and keep credentials out of URLs, logs,
fixtures and source control. `introspectToken()` rejects when HubSpot returns an
inactive token.
24 changes: 24 additions & 0 deletions packages/v1-ready/hubspot/docs/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changes and support

This snapshot documents `{{PACKAGE}}@{{VERSION}}`. Its release record contains
the immutable source input commit, the exact package metadata after release
versioning, generator identity, bundle digest, and verified npm integrity.
The catalog links to the package changelog at that source input commit.

Before upgrading, compare the target version's public-method reference and
limitations and read its package changelog. A provider API change and a wrapper
package release are separate version dimensions. The OAuth implementation in
this source uses date-based token endpoints while many CRM methods retain v3
paths and association methods retain v4 paths.

This first docs pilot does not backfill earlier package versions or claim that
current code describes those historical releases. It introduces no authentication
or runtime API migration. Future breaking wrapper changes must carry migration
instructions alongside their release notes.

The package is MIT licensed. Report wrapper and documentation issues to the
[module library issue tracker](https://github.com/friggframework/api-module-library/issues)
with exact package/Frigg versions, method and redacted reproduction. Send HubSpot
account, app-entitlement or service incidents through the provider's support
channel. Never include client secrets, tokens, customer records or signed webhook
bodies in a public issue.
6 changes: 6 additions & 0 deletions packages/v1-ready/hubspot/docs/first-call.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

module.exports = async function firstCall(Api, accessToken) {
const api = new Api({access_token: accessToken});
return api.listContacts({limit: 1, properties: ['email']});
};
39 changes: 39 additions & 0 deletions packages/v1-ready/hubspot/docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# First successful call

Install `{{PACKAGE}}@{{VERSION}}` with a compatible Frigg dependency
(`{{FRIGG_RANGE}}`). Obtain an OAuth access token for an account whose app has
`crm.objects.contacts.read`, following [OAuth setup](authentication.md).

Set `HUBSPOT_ACCESS_TOKEN` in your shell or secret manager, then run this read-only
example in a Node application:

```javascript
const {Api} = require('{{PACKAGE}}');

async function main() {
const api = new Api({access_token: process.env.HUBSPOT_ACCESS_TOKEN});
const page = await api.listContacts({limit: 1, properties: ['email']});
console.log(page.results.map(contact => contact.id));
}

main().catch(() => {
console.error('HubSpot contact read failed');
process.exitCode = 1;
});
```

A successful call resolves to one provider response, normally with `results` and
an optional `paging.next.after` cursor. An empty account can return an empty
`results` array. Providing `properties` avoids the module's additional property
discovery request. This example uses an already-issued token; it does not exchange
an authorization code or configure automatic refresh persistence.

The adjacent `first-call.cjs` is used by the offline fixture test. That check
exercises this module's method with a fake Requester and verifies URL and query
selection, omitted first-page cursors, and the returned fixture. It does not test
a live HubSpot account or the installed Frigg OAuth implementation.

For a Frigg application, register `Definition` through the application's module
binding and use the managed authorization flow so credentials can be persisted.
Treat access tokens and contact data as secrets or customer data; avoid logging
responses from production accounts merely to verify a connection.
66 changes: 66 additions & 0 deletions packages/v1-ready/hubspot/docs/operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Operating behavior and limitations

## Pagination, rate limits and errors

The wrapper generally returns one provider response per call. For contacts, pass
`limit`, `properties`, and then the previous page's `paging.next.after` to
`listContacts()`. The first page omits an empty cursor. Passing `properties`
avoids an extra property-discovery request. `listDeals()` merges the supplied
page object into its query and fetches all deal properties before the page request.
Some methods accept query strings rather than objects; use the exact signature in
the [reference](reference.md). There is no universal auto-pagination helper.

This module defines no provider-specific rate-limit policy. The installed Frigg
Requester handles HTTP transport and any generic retries. Apply bounded retries
and provider-aware pacing in the application, especially around writes and batch
calls. Do not infer newer Frigg behavior from this package's declared dependency
range. HubSpot scope, product entitlement, quota and transport errors can reject
calls. Returned shapes and empty-body behavior vary by method; see the reference
and [HubSpot API usage guidance](https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines).

## Webhook extension

The package exports `extensions.webhooks`, a reusable integration extension with
a DB-free receiver and queued portal-to-integration resolution. Bind it using
the Frigg extension contract:

```javascript
extensions: {
hubspotWebhooks: {
extension: hubspot.extensions.webhooks,
handlers: {HUBSPOT_WEBHOOK: 'onHubSpotEvent'},
},
}
```

The consuming integration must provide `createFriggCommands({integrationClass})`
as its `commands`, a queue worker, a HubSpot module binding and an idempotent
`onHubSpotEvent({data})` handler. The complete source-package README includes the
integration class example. This fragment is a binding, not a standalone app.

Register the app-level target URL in HubSpot, including the integration name and
binding key: `POST /api/<integration-name>-integration/hubspotWebhooks/webhooks`.
Set `HUBSPOT_CLIENT_SECRET` on the receiver. The shipped verifier requires the v3
signature and timestamp headers, computes HMAC-SHA256 over method, reconstructed
URI, raw body and timestamp, compares the base64 signature, and allows at most
five minutes of timestamp skew in either direction. Preserve the raw request
body and correct public URI through the HTTP adapter. Do not disable validation
to accommodate a proxy mismatch. See
[HubSpot request validation](https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/request-validation).

`HUBSPOT_WEBHOOK_RECEIVED` verifies the request and queues resolution per event.
The worker resolves `portalId` to the owning integration and queues
`HUBSPOT_WEBHOOK` with that integration's credentials. Missing portals or unmatched
integrations are skipped; ambiguous integration resolution throws. Delivery may
repeat: deduplicate business operations using event identifiers or a stable
composite key. This package ships no separate provider-side project artifact.

## Known source limitations

The pilot records the actual wrapper rather than promising every exported method
is production-ready. In this source, `updateDeal()` references a URL key that is
not defined, `createTimelineEvent()` passes positional arguments to an options
object helper, and `getCustomObjectSchema()` ignores its `query` argument.
Legacy contact-list and owner endpoints coexist with newer APIs. Review the
release-specific reference before use. Fixing these behaviors needs separate code
changes and validation; this documentation work does not silently change them.
Loading
Loading