@ascflow/partner-sdk
v1.0.9
Published
Official SDK for partners integrating with ASCFlow's iframe SSO. Generates short-lived embed URLs with HMAC-signed requests.
Maintainers
Readme
@ascflow/partner-sdk
Official Node.js SDK for partners integrating with ASCFlow's iframe SSO.
Generates short-lived, single-use embed URLs that you render inside an
<iframe> so your end users land in ASCFlow already authenticated — without
ever logging in twice.
Table of Contents
- What this SDK does
- Installation
- Quick start
- Configuration
- Embedding the iframe
- API reference
- Error handling
- Retries and idempotency
- How requests are signed
- Security
- Troubleshooting
- TypeScript
- Versioning and support
- License
What this SDK does
ASCFlow exposes a single endpoint for partner SSO:
POST /partner/auth/embed-urlCalling it requires:
- An HMAC-SHA256 signature over a canonical string built from the request
- An API key identifying the partner
- An Origin registered with ASCFlow for that partner
- A short-lived idempotency key to safely retry on network failure
This SDK does all of that for you and returns a one-shot embedUrl that you
drop into an <iframe>. It runs on your server (Node.js), never the
browser — your secret must never leave the backend.
┌──────────────────────────────────────────┐
│ Your partner backend (Node.js) │
│ │
end user ─── HTTP ─► ascflow-partner-sdk.createEmbedUrl() │
│ │ │
│ ▼ HMAC-signed POST │
│ ┌──────────────────────────────┐ │
│ │ ASCFlow gateway │ │
│ │ POST /partner/auth/embed-url │ │
│ └──────────────────────────────┘ │
│ │ │
│ ▼ │
│ { embedUrl, expiresIn } │
│ │
│ Render <iframe src={embedUrl} /> │
└──────────────────────────────────────────┘Don't want to use the SDK? Partners on other stacks (PHP, Python, Go, Ruby, Java...) can implement the same protocol by hand following docs/INTEGRACAO_SEM_SDK.pt-BR.md (Portuguese, with copy-pasteable examples in 7 languages and a fixed test vector).
Installation
npm install @ascflow/partner-sdk
# or
pnpm add @ascflow/partner-sdk
# or
yarn add @ascflow/partner-sdkRequirements
- Node.js 18 or later (uses the global
fetch) - TypeScript 5.x is supported but not required
Quick start
import { AscflowPartnerClient } from '@ascflow/partner-sdk';
// Reads ASCFLOW_PARTNER_* environment variables automatically.
const ascflow = new AscflowPartnerClient();
const { embedUrl, expiresIn } = await ascflow.createEmbedUrl({
email: '[email protected]',
name: 'Jane Doe',
cpf: '13166917731',
role: 'user',
redirectPath: '/flow/onboarding',
});
console.log(embedUrl); // https://app.ascflow.com/embed?code=...&r=%2Fflow%2Fonboarding
console.log(expiresIn); // 90 (seconds)A complete Express handler:
import express from 'express';
import { AscflowPartnerClient, AscflowError } from '@ascflow/partner-sdk';
const app = express();
const ascflow = new AscflowPartnerClient();
app.get('/render-ascflow', async (req, res) => {
// Authenticate and authorize the request on YOUR side first.
const user = await getCurrentUser(req);
if (!user) return res.status(401).end();
try {
const { embedUrl, expiresIn } = await ascflow.createEmbedUrl({
email: user.email,
name: user.fullName,
cpf: user.cpf,
role: user.role,
redirectPath: typeof req.query.r === 'string' ? req.query.r : '/',
});
res.json({ embedUrl, expiresIn });
} catch (err) {
if (err instanceof AscflowError) {
return res.status(502).json({ error: err.code, message: err.message });
}
throw err;
}
});Configuration
The client reads its configuration from environment variables by default. You can override any field by passing it to the constructor.
Environment variables
| Variable | Required | Description |
|---|:---:|---|
| ASCFLOW_PARTNER_API_KEY | ✅ | Your public partner identifier. |
| ASCFLOW_PARTNER_SECRET | ✅ | The signing secret. Server-side only. |
| ASCFLOW_PARTNER_ORIGIN | ✅ | Origin sent on every request. Must match an origin registered for your partner. |
| ASCFLOW_PARTNER_BASE_URL | ✅ | Gateway base URL (e.g. https://gateway.ascflow.com). |
| ASCFLOW_PARTNER_TIMEOUT_MS | – | Request timeout in ms. Default 10000. |
| ASCFLOW_PARTNER_MAX_RETRIES | – | Max automatic retries on 5xx / network errors. Default 2. |
A copy-pasteable .env.example ships with the package.
Constructor overrides
const ascflow = new AscflowPartnerClient({
apiKey: process.env.MY_KEY,
secret: process.env.MY_SECRET,
origin: 'https://app.partner.com',
baseUrl: 'https://gateway.ascflow.com',
timeoutMs: 5_000,
maxRetries: 3,
// Advanced — supply your own fetch (e.g. with a proxy agent or for tests).
fetch: customFetch,
// Default 'seconds'. Switch to 'milliseconds' only if your gateway expects it.
timestampFormat: 'seconds',
});⚠️ Timestamp format — the canonical string contains the timestamp, so the SDK and the gateway must agree on its format. The default
'seconds'matches the reference Postman pre-request script (Math.floor(Date.now() / 1000)). If your gateway validates milliseconds instead, settimestampFormat: 'milliseconds'.
Embedding the iframe
Once you have an embedUrl, render it on the page where the partner wants
ASCFlow to appear:
<iframe
src="https://app.ascflow.com/embed?code=a3f8...&r=%2Fflow%2Fmeu-processo"
width="100%"
height="800"
frameborder="0"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
allow="clipboard-write"
></iframe>The sandbox attribute is your defense-in-depth control. Keep it as
restrictive as your use case allows. ASCFlow already enforces strict
Content-Security-Policy and frame-ancestors on its side.
API reference
new AscflowPartnerClient(config?)
Creates a client. With no argument, it reads everything from environment variables. See Configuration.
client.createEmbedUrl(input)
Generates a one-shot embed URL.
Input (CreateEmbedUrlInput)
| Field | Type | Required | Notes |
|---|---|:---:|---|
| email | string | ✅ | The end user's email. |
| name | string | ✅ | Display name shown inside ASCFlow. |
| cpf | string | ✅ | End user's CPF (Brazilian tax ID). Digits only, exactly 11 characters. |
| role | 'admin' \| 'user' | ✅ | Role of the end user inside ASCFlow. |
| redirectPath | string | – | Path inside ASCFlow to land on. Must start with / and must not start with //. Default '/'. |
| idempotencyKey | string | – | Custom idempotency key. Default: a random UUID. |
Returns (Promise<CreateEmbedUrlResponse>)
{
embedUrl: string; // URL to put in <iframe src>
expiresIn: number; // seconds until the URL is invalid (typically 60–120)
}Throws — see Error handling.
client.signedRequest({ method, path, body, idempotencyKey })
Lower-level helper for advanced use cases. Signs and sends an arbitrary request to the gateway with the same canonicalization rules. Most users do not need this.
Helpers (named exports)
For testing and tooling integrations, the SDK also exports:
import {
buildCanonicalString,
hmacSha256Hex,
sha256Hex,
normalizePath,
currentTimestamp,
} from '@ascflow/partner-sdk';These let you reproduce the exact wire format the SDK uses (useful when debugging signature mismatches, writing custom transports, or signing requests from a non-Node environment that does its own HTTP).
Error handling
Every SDK error extends AscflowError, which has these properties:
| Property | Type | Notes |
|---|---|---|
| code | string | Stable machine-readable code (e.g. AUTH_ERROR). |
| status | number? | HTTP status, when applicable. |
| requestId | string? | From the x-request-id response header, when present. |
| details | unknown | Parsed response body, for forensic logging. |
Subclasses you can instanceof-narrow:
| Class | When |
|---|---|
| AscflowConfigError | Missing or invalid SDK configuration. No request was sent. |
| AscflowValidationError | Local input validation failed (bad email, bad redirectPath, oversized body). No request was sent. |
| AscflowAuthError | HTTP 401 / 403. Signature, timestamp, origin, or partner status problem. |
| AscflowRateLimitError | HTTP 429. Has retryAfterSeconds parsed from Retry-After. |
| AscflowRequestError | Other 4xx. Usually means a request-shape problem. |
| AscflowServerError | HTTP 5xx after retries are exhausted. |
| AscflowNetworkError | Connection error or timeout after retries are exhausted. |
import {
AscflowAuthError,
AscflowRateLimitError,
AscflowError,
} from '@ascflow/partner-sdk';
try {
await ascflow.createEmbedUrl({ email, name });
} catch (err) {
if (err instanceof AscflowAuthError) {
logger.error({ requestId: err.requestId }, 'ASCFlow rejected our credentials');
return res.status(502).json({ error: 'sso_auth_failed' });
}
if (err instanceof AscflowRateLimitError) {
res.setHeader('Retry-After', String(err.retryAfterSeconds ?? 30));
return res.status(429).end();
}
if (err instanceof AscflowError) {
logger.error({ code: err.code, status: err.status }, err.message);
return res.status(502).json({ error: 'sso_unavailable' });
}
throw err;
}Retries and idempotency
The SDK retries automatically on:
- Network errors (DNS, connection refused, timeout)
- HTTP
408,425,429,500,502,503,504
Retries use exponential backoff with jitter and respect the Retry-After
header on 429. The default is 2 retries (maxRetries: 2); set to 0
to disable.
Every retry sends the same Idempotency-Key, so the gateway can dedupe
duplicates. If you don't supply one, the SDK generates a fresh UUID per
createEmbedUrl call. Supply your own only if you need request-level
idempotency across process restarts (e.g. you're queuing the call and
might recover-and-retry minutes later).
How requests are signed
The SDK builds and signs every request like this:
canonicalString = METHOD + "\n"
+ PATH + "\n"
+ TIMESTAMP + "\n"
+ SHA256_HEX(BODY)
X-Signature = HMAC_SHA256_HEX(secret, canonicalString)Headers sent:
Content-Type: application/json
Origin: <ASCFLOW_PARTNER_ORIGIN>
X-Partner-Key: <ASCFLOW_PARTNER_API_KEY>
X-Timestamp: <unix seconds>
X-Signature: <hex HMAC>
Idempotency-Key: <UUID, auto or caller-supplied>PATH is normalized: leading slash, no trailing slash (except for /),
no empty segments. BODY is hashed as the exact UTF-8 bytes of the
serialized JSON, so the SDK serializes it once and uses the same string
for both the hash and the request body. There is no "canonical JSON";
the bytes that are hashed are the bytes that are sent.
If you ever need to verify a signature manually:
import { buildCanonicalString, hmacSha256Hex } from '@ascflow/partner-sdk';
const canonical = buildCanonicalString({
method: 'POST',
path: '/partner/auth/embed-url',
timestamp: '1730000000',
body: '{"email":"[email protected]","name":"A","redirectPath":"/","cpf":"00000000000","role":"user"}',
});
const signature = hmacSha256Hex(secret, canonical);Security
In short: never expose your secret to the browser, never commit it, and rotate it through the ASCFlow admin panel. The SDK enforces good defaults but cannot stop you from leaking a secret you've copied somewhere unsafe.
For a deeper checklist — including handling redirectPath, idempotency,
secret rotation, and what to log — see SECURITY.md.
Troubleshooting
AscflowAuthError: invalid signature
In order of likelihood:
- Wrong secret. Check
ASCFLOW_PARTNER_SECRET. The secret is shown only once when generated; if you lost it, rotate to a new one. - Timestamp format mismatch. Default is seconds. If your gateway is
configured for milliseconds, set
timestampFormat: 'milliseconds'. - Body was modified after signing. This shouldn't happen with the
SDK alone, but it can if a proxy, middleware, or custom
fetchrewrites the body. The hash is computed over the exact bytes that go on the wire. - Path mismatch. If you put a custom
baseUrlwith a path prefix (e.g.https://gateway.ascflow.com/v1), make sure your gateway is configured to canonicalize the same way.
AscflowAuthError: origin not allowed
The Origin you sent is not on your partner's allowlist. Compare:
console.log(process.env.ASCFLOW_PARTNER_ORIGIN);against the value registered with ASCFlow. The match is exact — trailing slashes, scheme, and port all matter.
AscflowAuthError: timestamp expired
Your server clock is more than the allowed skew off real time (typically 60s). Run NTP on the host. The SDK uses the local clock; it has no way to compensate.
AscflowValidationError: redirectPath must not start with "//" ...
You're trying to pass an absolute URL or a protocol-relative path to
redirectPath. This is blocked locally as defense-in-depth — the gateway
would reject it anyway. Use a path beginning with a single /.
Requests work in Postman but fail with the SDK
Almost always one of:
- The Postman script uses seconds; your gateway is configured for
milliseconds, or vice-versa. Compare with the SDK's
timestampFormat. - The Postman body has different whitespace / key order than what
JSON.stringifyproduces. The SDK hashes the bytes it actually sends, so this is fine on the SDK side — the issue would be on the gateway if it validates against a re-serialized body. Confirm the gateway hashesreq.rawBody, notJSON.stringify(req.body).
TypeScript
Types ship with the package; no need to install @types/@ascflow/partner-sdk.
Both ESM (import) and CJS (require) consumers are supported.
import type {
AscflowPartnerClientConfig,
CreateEmbedUrlInput,
CreateEmbedUrlResponse,
} from '@ascflow/partner-sdk';Versioning and support
This SDK follows Semantic Versioning. The current
major version is 0.x, which means the API may change in minor releases
until 1.0.0. Pin to an exact version in production until then.
- Bug reports / questions: open an issue on GitHub.
- Security issues: see SECURITY.md for the disclosure process. Do not open a public issue for vulnerabilities.
License
MIT © ASCFlow
