TypeScript SDK for the ISECure WS Channel API.
The checked-in OpenAPI contract is wsapi_v2.json. The live source at https://isecure.fi/wsapi_v2.json currently matches this repository copy and reports API version v2.6.0.
npm install isecure-ts-clientimport { WSChannel } from "isecure-ts-client";- Install
isecure-ts-client. - Configure account, bank, endpoint, password, and RSA public key values.
- Create
WSChannel. - Call
register()for first-time account setup, orlogin()for an existing account. - Complete any returned auth state: MFA, email verification, or phone verification.
- Use supported operations such as
listFiles,uploadFile, anduploadPgpKey.
| Environment variable | Required | Description |
|---|---|---|
ISECURE_BASE_URL |
No | API endpoint. Defaults to https://ws-api.test.isecure.fi/v2 in examples. |
ISECURE_API_KEY |
No | Existing integrator API key. Use 0 or omit for initial integrator registration. |
ISECURE_COMPANY |
Yes | Company name for registration. |
ISECURE_NAME |
Yes | Full user name for registration. |
ISECURE_EMAIL |
Yes | Account email address. |
ISECURE_PHONE |
Yes | Phone number with country code, for example +358401234567. |
ISECURE_PASSWORD |
Yes | Account password. |
ISECURE_PUBLIC_KEY_PEM |
Yes | ISECure RSA public key in PEM format. |
ISECURE_MODE |
No | admin or data. Defaults to data in examples. |
ISECURE_BANK |
No | Bank identifier. Defaults to nordea in examples. |
const client = new WSChannel({
ApiKey: process.env.ISECURE_API_KEY ?? "0",
Company: "Example Company",
Name: "Example User",
Password: process.env.ISECURE_PASSWORD!,
Phone: "+358401234567",
PublicKey: process.env.ISECURE_PUBLIC_KEY_PEM!,
BaseUrl: "https://ws-api.test.isecure.fi/v2",
Email: "user@example.test",
Mode: "data",
Bank: "nordea",
});
const state = await client.login();
if (state.status === "authenticated") {
const files = await client.listFiles({ Status: "ALL" });
console.log(files.FileDescriptors);
}const registration = await client.register();
console.log(registration.ApiKey);
let state = await client.login();
while (state.status !== "authenticated") {
if (state.status === "needs_mfa") {
state = await client.submitMfaCode("123456");
continue;
}
if (state.status === "needs_email_verification") {
await client.verifyEmail("123456");
state = await client.login();
continue;
}
if (state.status === "needs_phone_verification") {
await client.verifyPhone("123456");
state = await client.login();
continue;
}
if (state.status === "failed") {
throw new Error(state.responseText);
}
state = await client.login();
}Admin login and first-time registration may require MFA, email, or phone verification. The SDK returns typed auth states instead of reading from the terminal:
const state = await client.login();
if (state.status === "needs_mfa") {
// state.method is "sms" or "totp" (Google Authenticator) so you can label the prompt.
await client.submitMfaCode("123456");
}
if (state.status === "needs_email_verification") {
await client.verifyEmail("123456");
}
if (state.status === "needs_phone_verification") {
await client.verifyPhone("123456");
}Request enrollment while completing an admin login, render the returned QR
(otpauthUri) or secret, then confirm the first code. TOTP becomes the preferred
factor; SMS stays enabled as a fallback.
// Complete login with the SMS code AND ask to set up TOTP in one step:
const state = await client.submitMfaCode(smsCode, { setupTotp: true });
if (state.status === "authenticated" && state.totpEnrollment) {
const { secret, otpauthUri, accessToken } = state.totpEnrollment;
// Render otpauthUri as a QR code (or show `secret` for manual entry).
// accessToken is held in memory only — do not persist it.
await client.verifyTotp(accessToken, codeFromAuthenticatorApp);
}For CLI scripts, pass a prompt adapter. loginWithPrompt drives the whole MFA → email → phone verification machine to completion and is bounded, so you never re-implement the verify/re-login loop yourself:
const state = await client.loginWithPrompt({
requestMfaCode: async () => "...",
requestEmailCode: async () => "...",
requestPhoneCode: async () => "...",
});
if (state.status === "authenticated") {
// ready to call authenticated operations
} else if (state.status === "stalled") {
// an accepted verification did not advance login (e.g. backend did not flip
// confirmation). `state.step` names the stuck step instead of looping.
console.error(`Login stalled on ${state.step} after ${state.transitions} steps`);
} else if (state.status === "failed") {
// `state.reason` is a discriminable AuthErrorReason such as "invalid_code",
// "expired_code", "too_many_attempts", or "missing_access_token".
console.error(`Login failed (${state.reason}): ${state.responseText}`);
}Verification helpers (verifyEmail, verifyPhone) and classifyVerificationResponse return the same typed failed state with an AuthErrorReason, so invalid/expired codes, rate limiting, and already-verified cases are discriminable rather than collapsed into a single error string.
Logging is opt-in. By default the SDK uses a no-op logger and emits nothing. Inject a logger and keep LogLevel at debug (the default) to get redacted request/response debug lines for every call — secrets, tokens, and one-time codes are stripped before logging:
const client = new WSChannel(
{ ...props, LogLevel: "debug" },
{
logger: {
debug: (message, meta) => console.debug(message, meta),
info: () => {},
warn: () => {},
error: () => {},
},
},
);Set LogLevel: "silent" (or omit the logger) to disable transport logging. The redaction lives in LoggingTransport, which wraps whatever transport you provide, so a custom transport is logged too.
Redaction strips known secrets and PII (tokens, passwords, codes, email, phone, name), masks token-like values regardless of field name, and removes emails from logged URLs. For low-trust log sinks, pass redaction: "strict" (via WSChannelOptions or LoggingTransportOptions) to redact everything except an allowlist of known-safe fields.
Logical failures returned by the API as HTTP 200 with ResponseCode !== "00" surface through the typed auth/verification states described above. Transport-level failures throw a typed error hierarchy instead of raw AxiosErrors:
ISecureHttpError— a non-2xx response, exposingstatus,responseCode,responseText,requestId(quote this in support tickets), and the rawbody.ISecureNetworkError— a network failure or timeout (timedOut,code,cause).ISecureAbortError— a request cancelled viaAbortSignal.
All extend ISecureError; use isISecureError(err) to narrow.
import { isISecureError, ISecureHttpError } from "isecure-ts-client";
try {
await client.listFiles({ Status: "ALL" });
} catch (err) {
if (err instanceof ISecureHttpError) {
console.error(`HTTP ${err.status} (RequestId ${err.requestId}): ${err.responseText}`);
} else if (isISecureError(err)) {
console.error(err.message);
}
}The default AxiosTransport applies production defaults — a 30s timeout and bounded exponential-backoff retries (with jitter and Retry-After support) for transient failures (network errors, 408/425/429/5xx). Tune them via AxiosTransportOptions:
const client = new WSChannel(props, {
transport: new AxiosTransport({ timeoutMs: 10_000, retries: 3 }),
});The transport also honors an AbortSignal on each TransportRequest (cancelling an in-flight request or a pending retry backoff), which custom transports and integrations can use directly.
The SDK tracks the id-token expiry returned at login. Inspect it with client.isAuthenticated(), client.isSessionExpired(), and client.sessionExpiresAt. When an authenticated call is made on an expired session, the SDK invokes an optional refresh hook so you can re-establish the session in one place instead of handling 401s everywhere:
const client = new WSChannel(props, {
expirySkewMs: 30_000, // refresh 30s early
onSessionExpired: async (channel) => {
await channel.loginWithPrompt(promptAdapter);
},
});Without a hook, an authenticated call on an expired session throws ISecureError rather than sending a request that would 401. logout() always works, even when expired.
The SDK supports Node.js and modern browser bundlers. Password challenge encryption uses WebCrypto-compatible RSA-OAEP with SHA-1, so browser runtimes must provide globalThis.crypto.subtle.
Browser applications also need the ISECure WS API endpoint to allow the application origin with CORS. Avoid exposing production passwords, API keys, or id tokens in untrusted browser code; for most customer-facing web apps, run this SDK on your backend and call that backend from the browser.
The runnable terminal implementation lives in examples/full-workflow/full-workflow.ts.
Generated request and response types are built from wsapi_v2.json with swagger2openapi and openapi-typescript.
Currently supported:
InitRegisterRegisterInitPasswordResetPasswordResetInitLoginLoginLoginMFAVerifyEmailVerifyPhoneListCertsConfigCertsShareCertsUnshareCertsExportCertImportCertEnrollCertUploadKeyUploadFileListFilesDownloadFileDeleteFileListAccountsListKeysDeleteKeyLogout
All operations in wsapi_v2.json are now represented by WSChannel methods. The operation list is exported as SUPPORTED_OPERATIONS; UNSUPPORTED_OPERATIONS is empty.
yarn install --frozen-lockfile
yarn audit
yarn format:check
yarn lint
yarn typecheck
yarn test
yarn browser:check
yarn pack:checkImportant gates:
yarn generate:typesconverts Swagger 2.0 to OpenAPI 3.0 and generates TypeScript types.yarn auditchecks dependency advisories.yarn format:checkchecks Prettier formatting.yarn lintruns ESLint with type-aware rules.yarn typecheckruns strict TypeScript for the SDK and examples.yarn testruns Vitest with coverage thresholds.yarn browser:checkverifies the package entrypoint bundles for browser targets.yarn pack:checkverifies the library-only package payload.
Examples compile separately to dist-examples; they are not part of the npm package payload.
Semantic versions are prepared by Release Please from conventional commits:
fix: ...creates a patch release.feat: ...creates a minor release.feat!: ...orBREAKING CHANGE:creates a major release.
Release Please opens a version/changelog PR. Merging that PR creates the GitHub release, and the publish workflow publishes the package to npm.
Fully automated publishing requires repository secret RELEASE_PLEASE_TOKEN with a fine-grained GitHub token that can write contents, pull requests, and issues. The release workflow intentionally fails without that secret, because GitHub releases created by GITHUB_TOKEN do not trigger the npm publish workflow.
The npm package must also be configured with a trusted publisher:
- Publisher: GitHub Actions
- Organization or user:
dforsber - Repository:
isecure-ts-client - Workflow filename:
publish.yml - Environment name:
npm - Allowed action:
npm publish