From f1ff302e02961442d77f12a210f517584e50f156 Mon Sep 17 00:00:00 2001 From: Eduardo Ferreira Marques Cavalcante Date: Wed, 22 Jul 2026 17:45:17 -0300 Subject: [PATCH] update to 2.0.0 --- .../update-cloudhub-node-client/SKILL.md | 177 +++++ .config/dotnet-tools.json | 13 + .gitattributes | 3 + .gitignore | 5 + README.md | 2 +- cloudhub-client.js | 74 -- cloudhub-client.ts | 101 ++- dist/cloudhub-client.d.ts | 34 +- dist/cloudhub-client.js | 74 +- dist/rest-client.d.ts | 5 +- dist/rest-client.js | 16 +- dist/types.d.ts | 78 ++- dist/types.js | 31 +- openapi/cloudhub.json | 631 ++++++++++++++++++ package.json | 12 +- rest-client.js | 135 ---- rest-client.ts | 22 +- scripts/Update-NodeClient.ps1 | 146 ++++ test/cloudhub.test.js | 169 +++++ types.js | 10 - types.ts | 100 ++- 21 files changed, 1527 insertions(+), 311 deletions(-) create mode 100644 .claude/skills/update-cloudhub-node-client/SKILL.md create mode 100644 .config/dotnet-tools.json create mode 100644 .gitattributes delete mode 100644 cloudhub-client.js create mode 100644 openapi/cloudhub.json delete mode 100644 rest-client.js create mode 100644 scripts/Update-NodeClient.ps1 create mode 100644 test/cloudhub.test.js delete mode 100644 types.js diff --git a/.claude/skills/update-cloudhub-node-client/SKILL.md b/.claude/skills/update-cloudhub-node-client/SKILL.md new file mode 100644 index 0000000..b85e15e --- /dev/null +++ b/.claude/skills/update-cloudhub-node-client/SKILL.md @@ -0,0 +1,177 @@ +--- +name: update-cloudhub-node-client +description: > + Re-sync this Node client (cloudhubNodeClient, npm package cloudhub-client) with the CloudHub .NET + API: capture the latest OpenAPI spec from CloudHub, diff it, HAND-APPLY the changes to the + hand-written TypeScript sources (cloudhub-client.ts, rest-client.ts, types.ts), verify + (tsc --noEmit + npm test), and bump the package.json version. Use whenever the client must mirror + new/changed CloudHub endpoints or models, after CloudHub is updated, or when asked to + "update / regenerate / sync the cloudhub node client". +--- + +# Update the CloudHub Node client + +This client is **hand-written TypeScript** — there is **no code generator** (unlike the sibling +`cloudhub-java-client`, which is OpenAPI-Generator output; it is the same *kind* of client as the +`cloudHubPhpClient`). "Syncing" means: capture the CloudHub OpenAPI spec, read its diff, and **edit +the `.ts` sources by hand** to match. The spec is the source of truth and the change detector; you +supply all the code. + +## How the pieces fit (read first) + +- `openapi/cloudhub.json` is the committed **source of truth**. `git diff` on it = "what changed upstream". +- All library code is **hand-maintained** and lives at the repo root: + - `types.ts` — interfaces + string enums (the data contracts). + - `rest-client.ts` — `RestClient`, an **axios** wrapper. Its axios instance is created with the + `x-api-key` header, so **every** call is authenticated. (An optional 3rd constructor arg injects + an axios adapter; used only by tests — null in production.) + - `cloudhub-client.ts` — `CloudHubClient`, the public API (one method per endpoint). + - `index.ts` — the barrel that re-exports all three. + There are no generated files and nothing to protect from a generator; instead, **preserve backward + compatibility** of the exported class/interface/enum names and method signatures — the + PkiSuiteSamples nodejs routes depend on them. +- Request/response shapes are **interfaces**, not classes, because consumers pass **plain object + literals** to the client (e.g. `createSessionAsync({ identifier, redirectUri, type })`). Enums are + real `enum`s because callers reference members by name (`TrustServiceSessionTypes.SingleSignature`). +- `scripts/Update-NodeClient.ps1` does the two mechanical halves (capture spec; typecheck + test). + The manual editing happens in between and is what this skill guides. +- The version is **package.json-driven** — set `"version"` and `npm publish`. (This is the opposite + of the PHP client, which is versioned by git tag.) `dist/` **is committed** (consumers install this + package as a folder, with no build step), so always `npm run build` after editing. + +### Current surface (as of CloudHub 2.0.0) +`CloudHubClient` methods: `createSessionAsync`, `getCertificateAsync`, `signHashAsync` (the three the +PkiSuiteSamples node sample uses), plus 2.0.0 additions `createServiceSessionAsync`, +`getServiceAvailabilityAsync`, `getCustomStateAsync`, `getCertificateModelAsync`. Types/enums: +`SessionCreateRequest`, `ServiceSessionCreateRequest`, `ServiceSessionCreateResponse`, `SessionModel`, +`SignHashRequest`, `TrustServiceAuthParametersModel`, `TrustServiceInfoModel`, +`TrustServiceSessionTypes`, `IdentifierTypes`, `GetServiceAvailabilityResponse`, `CertificateModel`. +HTTP goes through `RestClient` (axios), which sends the `x-api-key` header on every call. + +> `createServiceSessionAsync(name, ServiceSessionCreateRequest)` → `POST /api/sessions/services/{name}` +> starts a session against one named trust service (e.g. `"safeid"`) and needs **no CPF/CNPJ** — the +> provider identifies the signer during its own auth flow (e.g. a QR code). Contrast with +> `createSessionAsync` (`POST /api/sessions`), which either discovers services by identifier or, when +> given an empty identifier + no `discover` flag, returns every configured service. +> +> The client deliberately implements only the endpoints its consumers need — it does **not** cover +> the entire CloudHub API. The one not-yet-implemented 2.0 endpoint is `GET /api/sessions/services` +> (list `TrustServiceInfoModel[]` by identifier, no session/authUrl). Add it only when a consumer +> needs it (see step 4). + +## Prerequisites + +- **.NET SDK** matching CloudHub's target framework (currently `net10.0`) to build + capture the spec. + The `swashbuckle.aspnetcore.cli` tool is pinned in `.config/dotnet-tools.json` (version 10.1.7). +- The **CloudHub repo** checked out, by default a sibling folder `../cloudhub`. +- For verification: **Node.js 18+** and **npm** (the test suite uses the built-in `node:test` runner — + no external framework). If absent, you can still capture the spec and hand-edit; verification + degrades to a manual type review + flags it. + +## Procedure + +### 1. Capture the new spec +Capture-only first, so you can inspect the diff before editing: +``` +pwsh scripts/Update-NodeClient.ps1 -SpecOnly +``` +This builds CloudHub, then writes `openapi/cloudhub.json` via the Swashbuckle CLI (`dotnet swagger +tofile ... v1`). If build/capture fails, see **Troubleshooting**. + +### 2. Review what changed upstream +``` +git diff -- openapi/cloudhub.json +``` +Write a short human summary grouped as: +- **Added** paths / schemas / properties +- **Removed** paths / schemas / properties ← breaking +- **Changed** types, `required`, enum members, `format` ← often breaking + +### 3. Classify the change → decide the version bump +Use [semver](https://semver.org) against the current `package.json` `"version"`: + +| Change | Bump | +|---|---| +| Only additive (new endpoints/models/optional fields) | **minor** | +| Removed/renamed endpoint, field, or enum member; a property became `required`; a type or wire `format` changed; an enum's underlying type changed (e.g. integer→string) | **major** | +| Doc-only / cosmetic | **patch** | + +State the recommended new version and why. (CloudHub 2.0.0 was a **major** bump because +`TrustServiceSessionTypes` went from an integer TS enum to a **string** enum on the wire.) + +### 4. Map each spec change to a hand edit (this is the judgment core — FLAG, don't guess) +There is no generator, so **every** change is a manual edit. Walk the diff and apply: + +- **New / changed schema property** → edit the matching interface in `types.ts`. Add new fields as + **optional** (`field?: T`) so existing object-literal callers keep compiling. Mark nullable spec + fields as `T | null` and be tolerant when reading responses (don't assume arrays/objects exist — + the responses are returned raw from axios, so guard on the consuming side / type them `?`). +- **Enum change**: + - *String enum* (CloudHub 2.0's style): `export enum X { NAME = "NAME" }`. Just add/rename members. + - *Integer enum*: `export enum X { NAME = 1 }` — switching an enum between integer and string is a + **wire-format** change and therefore **breaking** (major). +- **New endpoint** → add a method on `CloudHubClient` mirroring the existing ones: build a **relative** + path (`` `api/...` ``; RestClient's axios instance already has `baseURL`), call + `this.client.get(endpoint)` / `this.client.post(endpoint, body)`, and type the return as the model + interface. `encodeURIComponent(...)` any path segment or query value. **Decide whether the endpoint + belongs in this thin client at all**: if no consumer (PkiSuiteSamples node, or the requester) needs + it, **flag it and leave it out** rather than growing untested surface. +- **Binary payloads** (`type: string, format: byte`): base64. Mirror `getCertificateAsync`, which + resolves to the base64 **string** (`Promise`) — do NOT type it `Uint32Array`/`Buffer`. +- **Removed endpoint/field** → confirm intentional; call it out as breaking; only delete if you're + sure no consumer depends on it (this is a published package). +- **New auth scheme** → `RestClient` hard-codes the `x-api-key` header on the axios instance; update + it if a new scheme is needed, but flag it for a human first. + +### 5. Verify +``` +pwsh scripts/Update-NodeClient.ps1 -SkipCloudHubBuild # re-capture (fast) + typecheck + test +# or, to only typecheck+test the current code: +pwsh scripts/Update-NodeClient.ps1 -VerifyOnly +``` +This runs `npm install`, `npx tsc --noEmit` (typecheck), then `npm test` (which builds `dist/` and +runs the `node:test` suite in `test/`). The tests inject a capturing axios adapter and assert the +`x-api-key` header is sent and that `TrustServiceSessionTypes.SingleSignature` serializes as the +**string** `"SingleSignature"`. Extend `test/cloudhub.test.js` when you add endpoints/models. +If Node/npm are unavailable, review types by eye and **say so** in the report. + +### 6. Finalize +- Bump `"version"` in `package.json` to the new semver, and update the install snippet in + `README.md`. (Do **not** rely on git tags — this package is versioned by `package.json`.) +- `npm run build` to regenerate the committed `dist/`. Keep the stray top-level `.js` gone (the + `/*.js` gitignore rule prevents stale duplicates from reappearing next to the `.ts` sources). +- Re-run `git status` / `git diff` and sanity-check that only intended files changed and the public + API stayed backward-compatible (exported names + `new CloudHubClient(baseURL, apiKey)` and the + existing method signatures unchanged). + +### 7. Report +Tell the user: the new version, the upstream-change summary, each flagged judgment item and how you +resolved it, and the verification result. Do **not** commit/push/tag unless asked. + +## Verifying the result +- `npx tsc --noEmit` clean; `npm run build` regenerates `dist/` without errors. +- `npm test` green (x-api-key header sent + string-enum wire format). +- Backward compatibility intact: `new CloudHubClient(baseURL, apiKey)` and the existing + `createSessionAsync` / `getCertificateAsync` / `signHashAsync` signatures unchanged; the + PkiSuiteSamples node cloudhub routes still work against the new build. +- Idempotence: re-running `-SpecOnly` with no upstream change leaves an empty `git diff` on + `openapi/cloudhub.json`. + +## Troubleshooting +- **CloudHub build fails on restore**: the private Lacuna NuGet feed isn't configured. Add it + (`dotnet nuget add source ...`) or build on a machine with a warm package cache. If you only have a + pre-exported `swagger.json`, copy it to `openapi/cloudhub.json` and skip to step 2. (The sibling + `cloudHubPhpClient/openapi/cloudhub.json` is captured from the same server and is byte-identical.) +- **`dotnet swagger tofile` fails**: confirm the Swagger doc name is still `v1` + (`Site/Startup.cs` → `SwaggerDoc("v1", ...)`); pass the matching name as the last arg. Note the + spec's `info.version` is the literal string `"v1"` (the doc id), **not** the product version. +- **`node --test` finds no tests**: the script runs `node --test "test/**/*.test.js"`. Test files + must live under `test/` and end in `.test.js`, and require the COMPILED `../dist` (so a build must + run first — `npm test` does `npm run build` before the test run). +- **Types don't resolve in the test**: the tests import runtime values from `../dist` (interfaces + vanish at compile time — only enums, `RestClient`, and `CloudHubClient` exist at runtime). Assert + interface shapes with plain object literals, not imports. +- **Stray root `.js` reappear**: those were stale 1.x compiled output. The build emits only to + `dist/`; `/*.js` is git-ignored. Delete any that show up; don't commit them. +- **No Node on the machine**: capture + hand-edit still work; verification degrades to a manual type + review. Install Node.js 18+ to run `tsc --noEmit` + `npm test`. diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..1b12706 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "swashbuckle.aspnetcore.cli": { + "version": "10.1.7", + "commands": [ + "swagger" + ], + "rollForward": false + } + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0750d39 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Enable automatic line ending normalization (keeps diffs clean across Windows/Unix, so re-capturing +# openapi/cloudhub.json produces a stable, reviewable diff). +* text=auto diff --git a/.gitignore b/.gitignore index 6fb3f7e..e7aa5ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ node_modules/* package-lock.json + +# Stale compiled output used to sit next to the .ts sources at the repo root; the real build now +# emits only to dist/ (which IS committed, since consumers install this package as a folder with no +# build step). Ignore any top-level .js so those duplicates never come back. +/*.js diff --git a/README.md b/README.md index 62ee56a..3b0d52e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Or informing on your project's `package.json` file: { ... "dependencies": { - "cloudhub-client": "1.0.1" + "cloudhub-client": "2.0.0" } } diff --git a/cloudhub-client.js b/cloudhub-client.js deleted file mode 100644 index 66ef495..0000000 --- a/cloudhub-client.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (_) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -exports.__esModule = true; -exports.CloudHubClient = void 0; -var rest_client_1 = require("./rest-client"); -var CloudHubClient = /** @class */ (function () { - function CloudHubClient(baseURL, apiKey) { - var _this = this; - this.createSessionAsync = function (sessionCreateRequest) { return __awaiter(_this, void 0, void 0, function () { - var createSessionEndpoint, endpoint, res; - return __generator(this, function (_a) { - createSessionEndpoint = "api/sessions"; - endpoint = this.baseURL + createSessionEndpoint; - res = this.client.post(createSessionEndpoint, sessionCreateRequest); - return [2 /*return*/, res]; - }); - }); }; - this.getCertificateAsync = function (encodedSession) { return __awaiter(_this, void 0, void 0, function () { - var getCertificateEndpoint, endpoint; - return __generator(this, function (_a) { - getCertificateEndpoint = "api/sessions/certificate?session=".concat(encodedSession); - endpoint = this.baseURL + getCertificateEndpoint; - return [2 /*return*/, this.client.get(getCertificateEndpoint)]; - }); - }); }; - this.signHashAsync = function (signHashRequest) { return __awaiter(_this, void 0, void 0, function () { - var getCertificateEndpoint, endpoint; - return __generator(this, function (_a) { - getCertificateEndpoint = "api/sessions/sign-hash"; - endpoint = this.baseURL + getCertificateEndpoint; - return [2 /*return*/, this.client.post(getCertificateEndpoint, signHashRequest)]; - }); - }); }; - this.client = new rest_client_1.RestClient(baseURL, apiKey); - this.baseURL = baseURL; - } - return CloudHubClient; -}()); -exports.CloudHubClient = CloudHubClient; diff --git a/cloudhub-client.ts b/cloudhub-client.ts index 151e01b..8aaef54 100644 --- a/cloudhub-client.ts +++ b/cloudhub-client.ts @@ -1,33 +1,94 @@ -import axios, { AxiosInstance } from "axios"; +import { AxiosAdapter } from "axios"; import { RestClient } from "./rest-client"; -import { SessionCreateRequest, SessionModel, SignHashRequest } from "./types"; +import { + SessionCreateRequest, + SessionModel, + SignHashRequest, + ServiceSessionCreateRequest, + ServiceSessionCreateResponse, + GetServiceAvailabilityResponse, + CertificateModel, + IdentifierTypes, +} from "./types"; export class CloudHubClient { - private baseURL: string; protected client: RestClient; - constructor(baseURL: string, apiKey: string) { - this.client = new RestClient(baseURL, apiKey); - this.baseURL = baseURL; + // `adapter` is an optional axios adapter forwarded to RestClient; it is only used by unit tests + // (null in production), so `new CloudHubClient(baseURL, apiKey)` keeps working unchanged. + constructor(baseURL: string, apiKey: string, adapter?: AxiosAdapter) { + this.client = new RestClient(baseURL, apiKey, adapter); } - + + // Endpoints are relative: RestClient's axios instance is created with `baseURL`, so it prefixes + // the host on every request. + public createSessionAsync = async (sessionCreateRequest?: SessionCreateRequest): Promise => { - const createSessionEndpoint = "api/sessions"; - const endpoint = this.baseURL + createSessionEndpoint; - return Promise.resolve(this.client.post(createSessionEndpoint, sessionCreateRequest)); + const endpoint = "api/sessions"; + return this.client.post(endpoint, sessionCreateRequest); }; - public getCertificateAsync = async (encodedSession: string): Promise => { - const getCertificateEndpoint = `api/sessions/certificate?session=${encodedSession}`; - const endpoint = this.baseURL + getCertificateEndpoint; - return this.client.get(getCertificateEndpoint); - + /** + * CloudHub 2.0.0 - POST /api/sessions/services/{name} + * Creates a session against a single, named trust service (e.g. "safeid"), returning that + * service's auth parameters directly. Unlike createSessionAsync(), no CPF/CNPJ is required — the + * provider identifies the signer during its own authentication flow (e.g. a QR code). + */ + public createServiceSessionAsync = async (name: string, request: ServiceSessionCreateRequest): Promise => { + const endpoint = `api/sessions/services/${encodeURIComponent(name)}`; + return this.client.post(endpoint, request); }; - public signHashAsync = async (signHashRequest?: SignHashRequest): Promise => { - const getCertificateEndpoint = "api/sessions/sign-hash"; - const endpoint = this.baseURL + getCertificateEndpoint; - return this.client.post(getCertificateEndpoint, signHashRequest); + // NOTE: getCertificateAsync resolves to a base64-encoded certificate STRING (the endpoint's wire + // format is `string`/`byte`). The 1.x typings said `Uint32Array`, which was wrong — the + // PkiSuiteSamples routes already use the result as a base64 string (passed straight to Rest PKI). + public getCertificateAsync = async (encodedSession: string): Promise => { + const endpoint = `api/sessions/certificate?session=${encodedSession}`; + return this.client.get(endpoint); }; -} + // Resolves to a base64-encoded signature STRING (see the getCertificateAsync note above). + public signHashAsync = async (signHashRequest?: SignHashRequest): Promise => { + const endpoint = "api/sessions/sign-hash"; + return this.client.post(endpoint, signHashRequest); + }; + + /** + * CloudHub 2.0.0 - GET /api/sessions/services/{name}/availability + * Checks whether a given trust service is available for a signer (and, optionally, whether a + * certificate was found for the supplied identifier). + */ + public getServiceAvailabilityAsync = async (name: string, identifier?: string, identifierType?: IdentifierTypes): Promise => { + let endpoint = `api/sessions/services/${encodeURIComponent(name)}/availability`; + const query: string[] = []; + if (identifier !== undefined && identifier !== null) { + query.push(`identifier=${encodeURIComponent(identifier)}`); + } + if (identifierType !== undefined && identifierType !== null) { + query.push(`identifierType=${encodeURIComponent(identifierType)}`); + } + if (query.length > 0) { + endpoint += `?${query.join("&")}`; + } + return this.client.get(endpoint); + }; + + /** + * CloudHub 2.0.0 - GET /api/sessions/custom-state + * Returns the custom state string that was supplied on session creation. + */ + public getCustomStateAsync = async (session: string): Promise => { + const endpoint = `api/sessions/custom-state?session=${encodeURIComponent(session)}`; + return this.client.get(endpoint); + }; + + /** + * CloudHub 2.0.0 - GET /api/v2/sessions/certificate + * Structured certificate (content + alias + serviceName). The v1 getCertificateAsync() still + * returns the raw base64 certificate string for backward compatibility. + */ + public getCertificateModelAsync = async (session: string): Promise => { + const endpoint = `api/v2/sessions/certificate?session=${encodeURIComponent(session)}`; + return this.client.get(endpoint); + }; +} diff --git a/dist/cloudhub-client.d.ts b/dist/cloudhub-client.d.ts index 0b86196..0541042 100644 --- a/dist/cloudhub-client.d.ts +++ b/dist/cloudhub-client.d.ts @@ -1,10 +1,34 @@ +import { AxiosAdapter } from "axios"; import { RestClient } from "./rest-client"; -import { SessionCreateRequest, SessionModel, SignHashRequest } from "./types"; +import { SessionCreateRequest, SessionModel, SignHashRequest, ServiceSessionCreateRequest, ServiceSessionCreateResponse, GetServiceAvailabilityResponse, CertificateModel, IdentifierTypes } from "./types"; export declare class CloudHubClient { - private baseURL; protected client: RestClient; - constructor(baseURL: string, apiKey: string); + constructor(baseURL: string, apiKey: string, adapter?: AxiosAdapter); createSessionAsync: (sessionCreateRequest?: SessionCreateRequest) => Promise; - getCertificateAsync: (encodedSession: string) => Promise; - signHashAsync: (signHashRequest?: SignHashRequest) => Promise; + /** + * CloudHub 2.0.0 - POST /api/sessions/services/{name} + * Creates a session against a single, named trust service (e.g. "safeid"), returning that + * service's auth parameters directly. Unlike createSessionAsync(), no CPF/CNPJ is required — the + * provider identifies the signer during its own authentication flow (e.g. a QR code). + */ + createServiceSessionAsync: (name: string, request: ServiceSessionCreateRequest) => Promise; + getCertificateAsync: (encodedSession: string) => Promise; + signHashAsync: (signHashRequest?: SignHashRequest) => Promise; + /** + * CloudHub 2.0.0 - GET /api/sessions/services/{name}/availability + * Checks whether a given trust service is available for a signer (and, optionally, whether a + * certificate was found for the supplied identifier). + */ + getServiceAvailabilityAsync: (name: string, identifier?: string, identifierType?: IdentifierTypes) => Promise; + /** + * CloudHub 2.0.0 - GET /api/sessions/custom-state + * Returns the custom state string that was supplied on session creation. + */ + getCustomStateAsync: (session: string) => Promise; + /** + * CloudHub 2.0.0 - GET /api/v2/sessions/certificate + * Structured certificate (content + alias + serviceName). The v1 getCertificateAsync() still + * returns the raw base64 certificate string for backward compatibility. + */ + getCertificateModelAsync: (session: string) => Promise; } diff --git a/dist/cloudhub-client.js b/dist/cloudhub-client.js index d614224..325cc64 100644 --- a/dist/cloudhub-client.js +++ b/dist/cloudhub-client.js @@ -12,24 +12,74 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.CloudHubClient = void 0; const rest_client_1 = require("./rest-client"); class CloudHubClient { - constructor(baseURL, apiKey) { + // `adapter` is an optional axios adapter forwarded to RestClient; it is only used by unit tests + // (null in production), so `new CloudHubClient(baseURL, apiKey)` keeps working unchanged. + constructor(baseURL, apiKey, adapter) { + // Endpoints are relative: RestClient's axios instance is created with `baseURL`, so it prefixes + // the host on every request. this.createSessionAsync = (sessionCreateRequest) => __awaiter(this, void 0, void 0, function* () { - const createSessionEndpoint = "api/sessions"; - const endpoint = this.baseURL + createSessionEndpoint; - return Promise.resolve(this.client.post(createSessionEndpoint, sessionCreateRequest)); + const endpoint = "api/sessions"; + return this.client.post(endpoint, sessionCreateRequest); }); + /** + * CloudHub 2.0.0 - POST /api/sessions/services/{name} + * Creates a session against a single, named trust service (e.g. "safeid"), returning that + * service's auth parameters directly. Unlike createSessionAsync(), no CPF/CNPJ is required — the + * provider identifies the signer during its own authentication flow (e.g. a QR code). + */ + this.createServiceSessionAsync = (name, request) => __awaiter(this, void 0, void 0, function* () { + const endpoint = `api/sessions/services/${encodeURIComponent(name)}`; + return this.client.post(endpoint, request); + }); + // NOTE: getCertificateAsync resolves to a base64-encoded certificate STRING (the endpoint's wire + // format is `string`/`byte`). The 1.x typings said `Uint32Array`, which was wrong — the + // PkiSuiteSamples routes already use the result as a base64 string (passed straight to Rest PKI). this.getCertificateAsync = (encodedSession) => __awaiter(this, void 0, void 0, function* () { - const getCertificateEndpoint = `api/sessions/certificate?session=${encodedSession}`; - const endpoint = this.baseURL + getCertificateEndpoint; - return this.client.get(getCertificateEndpoint); + const endpoint = `api/sessions/certificate?session=${encodedSession}`; + return this.client.get(endpoint); }); + // Resolves to a base64-encoded signature STRING (see the getCertificateAsync note above). this.signHashAsync = (signHashRequest) => __awaiter(this, void 0, void 0, function* () { - const getCertificateEndpoint = "api/sessions/sign-hash"; - const endpoint = this.baseURL + getCertificateEndpoint; - return this.client.post(getCertificateEndpoint, signHashRequest); + const endpoint = "api/sessions/sign-hash"; + return this.client.post(endpoint, signHashRequest); + }); + /** + * CloudHub 2.0.0 - GET /api/sessions/services/{name}/availability + * Checks whether a given trust service is available for a signer (and, optionally, whether a + * certificate was found for the supplied identifier). + */ + this.getServiceAvailabilityAsync = (name, identifier, identifierType) => __awaiter(this, void 0, void 0, function* () { + let endpoint = `api/sessions/services/${encodeURIComponent(name)}/availability`; + const query = []; + if (identifier !== undefined && identifier !== null) { + query.push(`identifier=${encodeURIComponent(identifier)}`); + } + if (identifierType !== undefined && identifierType !== null) { + query.push(`identifierType=${encodeURIComponent(identifierType)}`); + } + if (query.length > 0) { + endpoint += `?${query.join("&")}`; + } + return this.client.get(endpoint); + }); + /** + * CloudHub 2.0.0 - GET /api/sessions/custom-state + * Returns the custom state string that was supplied on session creation. + */ + this.getCustomStateAsync = (session) => __awaiter(this, void 0, void 0, function* () { + const endpoint = `api/sessions/custom-state?session=${encodeURIComponent(session)}`; + return this.client.get(endpoint); + }); + /** + * CloudHub 2.0.0 - GET /api/v2/sessions/certificate + * Structured certificate (content + alias + serviceName). The v1 getCertificateAsync() still + * returns the raw base64 certificate string for backward compatibility. + */ + this.getCertificateModelAsync = (session) => __awaiter(this, void 0, void 0, function* () { + const endpoint = `api/v2/sessions/certificate?session=${encodeURIComponent(session)}`; + return this.client.get(endpoint); }); - this.client = new rest_client_1.RestClient(baseURL, apiKey); - this.baseURL = baseURL; + this.client = new rest_client_1.RestClient(baseURL, apiKey, adapter); } } exports.CloudHubClient = CloudHubClient; diff --git a/dist/rest-client.d.ts b/dist/rest-client.d.ts index 2c774e7..e32309e 100644 --- a/dist/rest-client.d.ts +++ b/dist/rest-client.d.ts @@ -1,10 +1,11 @@ -import { AxiosInstance } from "axios"; +import { AxiosInstance, AxiosAdapter } from "axios"; export declare class RestClient { private baseURL; private apiKey; + private adapter?; private client; private getClient; - constructor(baseURL: string, apiKey: string); + constructor(baseURL: string, apiKey: string, adapter?: AxiosAdapter); initClient(): AxiosInstance; private errorHandling; post(endpoint: string, request: any): Promise; diff --git a/dist/rest-client.js b/dist/rest-client.js index a256acf..748d4ab 100644 --- a/dist/rest-client.js +++ b/dist/rest-client.js @@ -15,7 +15,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.RestClient = void 0; const axios_1 = __importDefault(require("axios")); class RestClient { - constructor(baseURL, apiKey) { + getClient() { + return this.client != null ? this.client : this.initClient(); + } + constructor(baseURL, apiKey, adapter) { this.client = null; this.errorHandling = (error) => { if (axios_1.default.isAxiosError(error)) { @@ -33,18 +36,13 @@ class RestClient { }; this.baseURL = baseURL; this.apiKey = apiKey; + this.adapter = adapter; this.client = this.getClient(); } - getClient() { - return this.client != null ? this.client : this.initClient(); - } initClient() { - return axios_1.default.create({ - baseURL: this.baseURL, - headers: { + return axios_1.default.create(Object.assign({ baseURL: this.baseURL, headers: { "x-api-key": this.apiKey - }, - }); + } }, (this.adapter ? { adapter: this.adapter } : {}))); } post(endpoint, request) { return __awaiter(this, void 0, void 0, function* () { diff --git a/dist/types.d.ts b/dist/types.d.ts index 38cf394..cc2b136 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -1,27 +1,66 @@ +/** + * CloudHub 2.0.0: session types are STRING-valued on the wire (they were integers in 1.x). This is + * the breaking change behind the 2.0 major bump. Callers reference the members by name + * (TrustServiceSessionTypes.SingleSignature), so their code is unchanged — only the emitted value + * changes from `1` to `"SingleSignature"`. + */ +export declare enum TrustServiceSessionTypes { + SingleSignature = "SingleSignature", + MultiSignature = "MultiSignature", + SignatureSession = "SignatureSession", + AuthenticationSession = "AuthenticationSession" +} +/** + * CloudHub 2.0.0: signer identifier kind. Used by SessionCreateRequest.identifierType and the + * services/availability query. String-valued on the wire. + */ +export declare enum IdentifierTypes { + CPF = "CPF", + CNPJ = "CNPJ" +} export interface SessionCreateRequest { identifier?: string; + /** Required by the server (a session must have somewhere to redirect back to). */ redirectUri?: string; type?: TrustServiceSessionTypes; - lifetimeInSeconds: number; + /** Optional; a missing value means "no explicit lifetime" (the server applies its own default). */ + lifetimeInSeconds?: number; + identifierType?: IdentifierTypes; + customState?: string; + discover?: boolean; } -export declare enum TrustServiceSessionTypes { - SingleSignature = 1, - MultiSignature = 2, - SignatureSession = 3, - AuthenticationSession = 4 +/** + * CloudHub 2.0.0: request body of POST /api/sessions/services/{name} (create a session against a + * single, named trust service). Mirrors SessionCreateRequest WITHOUT `identifierType` — the service + * is named in the URL path instead. `identifier` is optional: omit it to start a service session + * with no CPF/CNPJ (the provider identifies the signer during its own auth flow, e.g. a QR code). + */ +export interface ServiceSessionCreateRequest { + /** Required by the server. */ + redirectUri?: string; + type?: TrustServiceSessionTypes; + identifier?: string; + lifetimeInSeconds?: number; + customState?: string; + discover?: boolean; } export interface SessionModel { services?: Array | null; } export interface TrustServiceAuthParametersModel { serviceInfo?: TrustServiceInfoModel; - authUrl?: string; + authUrl?: string | null; } +/** + * CloudHub 2.0.0: response of POST /api/sessions/services/{name}. Structurally identical to + * TrustServiceAuthParametersModel (the server declares it as a subclass of it). + */ +export type ServiceSessionCreateResponse = TrustServiceAuthParametersModel; export interface TrustServiceInfoModel { - serviceName: string; - provider: string; - endpoint: string; - badgeUrl: string; + serviceName?: string | null; + provider?: string | null; + endpoint?: string | null; + badgeUrl?: string | null; } export interface SignHashRequest { session: string; @@ -30,3 +69,20 @@ export interface SignHashRequest { digestAlgorithmOid?: string; certificateAlias?: string; } +/** + * CloudHub 2.0.0: response of GET /api/v2/sessions/certificate. `serviceName` was added in 2.0. The + * v1 getCertificateAsync() still returns the raw base64 certificate string for backward compatibility. + */ +export interface CertificateModel { + /** Base64-encoded certificate bytes. */ + content?: string | null; + alias?: string | null; + serviceName?: string | null; +} +/** + * CloudHub 2.0.0: response of GET /api/sessions/services/{name}/availability. + */ +export interface GetServiceAvailabilityResponse { + discoveryAvailable: boolean; + certificateFound?: boolean | null; +} diff --git a/dist/types.js b/dist/types.js index 6698905..3c9516e 100644 --- a/dist/types.js +++ b/dist/types.js @@ -1,10 +1,31 @@ "use strict"; +// types.ts — CloudHub client data contracts. +// +// These are kept as interfaces (not classes) because consumers pass plain object literals to the +// client, e.g. createSessionAsync({ identifier, redirectUri, type: TrustServiceSessionTypes.SingleSignature }) +// (see the PkiSuiteSamples nodejs routes). Enums stay as `enum` because callers reference the +// members by name (TrustServiceSessionTypes.SingleSignature). Object.defineProperty(exports, "__esModule", { value: true }); -exports.TrustServiceSessionTypes = void 0; +exports.IdentifierTypes = exports.TrustServiceSessionTypes = void 0; +/** + * CloudHub 2.0.0: session types are STRING-valued on the wire (they were integers in 1.x). This is + * the breaking change behind the 2.0 major bump. Callers reference the members by name + * (TrustServiceSessionTypes.SingleSignature), so their code is unchanged — only the emitted value + * changes from `1` to `"SingleSignature"`. + */ var TrustServiceSessionTypes; (function (TrustServiceSessionTypes) { - TrustServiceSessionTypes[TrustServiceSessionTypes["SingleSignature"] = 1] = "SingleSignature"; - TrustServiceSessionTypes[TrustServiceSessionTypes["MultiSignature"] = 2] = "MultiSignature"; - TrustServiceSessionTypes[TrustServiceSessionTypes["SignatureSession"] = 3] = "SignatureSession"; - TrustServiceSessionTypes[TrustServiceSessionTypes["AuthenticationSession"] = 4] = "AuthenticationSession"; + TrustServiceSessionTypes["SingleSignature"] = "SingleSignature"; + TrustServiceSessionTypes["MultiSignature"] = "MultiSignature"; + TrustServiceSessionTypes["SignatureSession"] = "SignatureSession"; + TrustServiceSessionTypes["AuthenticationSession"] = "AuthenticationSession"; })(TrustServiceSessionTypes = exports.TrustServiceSessionTypes || (exports.TrustServiceSessionTypes = {})); +/** + * CloudHub 2.0.0: signer identifier kind. Used by SessionCreateRequest.identifierType and the + * services/availability query. String-valued on the wire. + */ +var IdentifierTypes; +(function (IdentifierTypes) { + IdentifierTypes["CPF"] = "CPF"; + IdentifierTypes["CNPJ"] = "CNPJ"; +})(IdentifierTypes = exports.IdentifierTypes || (exports.IdentifierTypes = {})); diff --git a/openapi/cloudhub.json b/openapi/cloudhub.json new file mode 100644 index 0000000..3af92bf --- /dev/null +++ b/openapi/cloudhub.json @@ -0,0 +1,631 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "Cloudhub API", + "version": "v1" + }, + "paths": { + "/api/sessions/services": { + "get": { + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "identifier", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "identifierType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/IdentifierTypes" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrustServiceInfoModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrustServiceInfoModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrustServiceInfoModel" + } + } + } + } + } + } + } + }, + "/api/sessions/services/{name}/availability": { + "get": { + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "identifier", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "identifierType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/IdentifierTypes" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/GetServiceAvailabilityResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetServiceAvailabilityResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/GetServiceAvailabilityResponse" + } + } + } + } + } + } + }, + "/api/sessions": { + "post": { + "tags": [ + "Sessions" + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/SessionCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionCreateRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SessionCreateRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/SessionCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/SessionModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SessionModel" + } + } + } + } + } + } + }, + "/api/sessions/services/{name}": { + "post": { + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ServiceSessionCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceSessionCreateRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ServiceSessionCreateRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ServiceSessionCreateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ServiceSessionCreateResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceSessionCreateResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ServiceSessionCreateResponse" + } + } + } + } + } + } + }, + "/api/sessions/certificate": { + "get": { + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "session", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "application/json": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "text/json": { + "schema": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + }, + "/api/v2/sessions/certificate": { + "get": { + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "session", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/CertificateModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/CertificateModel" + } + } + } + } + } + } + }, + "/api/sessions/sign-hash": { + "post": { + "tags": [ + "Sessions" + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/SignHashRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignHashRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/SignHashRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/SignHashRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "application/json": { + "schema": { + "type": "string", + "format": "byte" + } + }, + "text/json": { + "schema": { + "type": "string", + "format": "byte" + } + } + } + } + } + } + }, + "/api/sessions/custom-state": { + "get": { + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "session", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CertificateModel": { + "type": "object", + "properties": { + "content": { + "type": "string", + "format": "byte", + "nullable": true + }, + "alias": { + "type": "string", + "nullable": true + }, + "serviceName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "GetServiceAvailabilityResponse": { + "type": "object", + "properties": { + "discoveryAvailable": { + "type": "boolean" + }, + "certificateFound": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "IdentifierTypes": { + "enum": [ + "CPF", + "CNPJ" + ], + "type": "string" + }, + "ServiceSessionCreateRequest": { + "required": [ + "redirectUri" + ], + "type": "object", + "properties": { + "identifier": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/TrustServiceSessionTypes" + }, + "redirectUri": { + "minLength": 1, + "type": "string" + }, + "lifetimeInSeconds": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "customState": { + "type": "string", + "nullable": true + }, + "discover": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "ServiceSessionCreateResponse": { + "type": "object", + "properties": { + "serviceInfo": { + "$ref": "#/components/schemas/TrustServiceInfoModel" + }, + "authUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "SessionCreateRequest": { + "required": [ + "redirectUri" + ], + "type": "object", + "properties": { + "identifierType": { + "$ref": "#/components/schemas/IdentifierTypes" + }, + "identifier": { + "type": "string", + "nullable": true + }, + "type": { + "$ref": "#/components/schemas/TrustServiceSessionTypes" + }, + "redirectUri": { + "minLength": 1, + "type": "string" + }, + "lifetimeInSeconds": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "customState": { + "type": "string", + "nullable": true + }, + "discover": { + "type": "boolean", + "nullable": true + } + }, + "additionalProperties": false + }, + "SessionModel": { + "type": "object", + "properties": { + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrustServiceAuthParametersModel" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "SignHashRequest": { + "required": [ + "hash", + "session" + ], + "type": "object", + "properties": { + "session": { + "minLength": 1, + "type": "string" + }, + "hash": { + "type": "string", + "format": "byte" + }, + "digestAlgorithm": { + "type": "string", + "nullable": true + }, + "digestAlgorithmOid": { + "type": "string", + "nullable": true + }, + "certificateAlias": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "TrustServiceAuthParametersModel": { + "type": "object", + "properties": { + "serviceInfo": { + "$ref": "#/components/schemas/TrustServiceInfoModel" + }, + "authUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "TrustServiceInfoModel": { + "type": "object", + "properties": { + "serviceName": { + "type": "string", + "nullable": true + }, + "provider": { + "type": "string", + "nullable": true + }, + "endpoint": { + "type": "string", + "nullable": true + }, + "badgeUrl": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "TrustServiceSessionTypes": { + "enum": [ + "SingleSignature", + "MultiSignature", + "SignatureSession", + "AuthenticationSession" + ], + "type": "string" + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "description": "Api Key authentication", + "name": "X-Api-Key", + "in": "header" + } + } + }, + "tags": [ + { + "name": "Sessions" + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index a639d56..3ae4440 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { "dependencies": { - "axios": "^1.2.2", - "typescript": "^4.9.4" + "axios": "^1.2.2" }, "name": "cloudhub-client", - "version": "1.0.0", + "version": "2.0.0", "description": "A client library for Lacuna's CloudHub API in Node.js", "main": "./dist/index.js", - "devDependencies": {}, + "types": "./dist/index.d.ts", + "devDependencies": { + "typescript": "^4.9.4" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "npm run build && node --test \"test/**/*.test.js\"", "prepublish": "npm run build", "build": "tsc --outdir dist/" }, diff --git a/rest-client.js b/rest-client.js deleted file mode 100644 index 3eafe7b..0000000 --- a/rest-client.js +++ /dev/null @@ -1,135 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (_) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -exports.__esModule = true; -exports.RestClient = void 0; -var axios_1 = require("axios"); -var RestClient = /** @class */ (function () { - function RestClient(baseURL, apiKey) { - this.client = null; - this.errorHandling = function (error) { - if (axios_1["default"].isAxiosError(error)) { - console.log(error.toJSON()); - // console.log("Error message:", error.message); - return error.message; - } - else { - console.log("unexpectedError: ", error); - return "an unexpected error occurred"; - } - }; - this.baseURL = baseURL; - this.apiKey = apiKey; - this.client = this.getClient(); - } - RestClient.prototype.getClient = function () { - return this.client != null ? this.client : this.initClient(); - }; - RestClient.prototype.initClient = function () { - return axios_1["default"].create({ - baseURL: this.baseURL, - headers: { - "x-api-key": this.apiKey - } - }); - }; - RestClient.prototype.post = function (endpoint, request) { - return __awaiter(this, void 0, void 0, function () { - var client, res, error_1; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - client = this.getClient(); - _a.label = 1; - case 1: - _a.trys.push([1, 3, , 4]); - return [4 /*yield*/, client.post(endpoint, request, { - headers: { - 'content-type': 'application/json', - Accept: 'application/json' - } - })]; - case 2: - res = _a.sent(); - if (res.status != 200) { - console.log("Deu errado aqui broder"); - console.log(res.data.code); - console.log(res.data.message); - return [2 /*return*/, JSON.parse(res.data)]; - } - else { - return [2 /*return*/, res.data]; - } - return [3 /*break*/, 4]; - case 3: - error_1 = _a.sent(); - throw this.errorHandling(error_1); - case 4: return [2 /*return*/]; - } - }); - }); - }; - RestClient.prototype.get = function (endpoint, params) { - return __awaiter(this, void 0, void 0, function () { - var client, res, error_2; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - client = this.getClient(); - _a.label = 1; - case 1: - _a.trys.push([1, 3, , 4]); - return [4 /*yield*/, client.get(endpoint, { - headers: { - 'content-type': 'application/json', - Accept: 'application/json' - } - })]; - case 2: - res = _a.sent(); - return [2 /*return*/, res.data]; - case 3: - error_2 = _a.sent(); - throw this.errorHandling(error_2); - case 4: return [2 /*return*/]; - } - }); - }); - }; - return RestClient; -}()); -exports.RestClient = RestClient; diff --git a/rest-client.ts b/rest-client.ts index b64638d..7463a32 100644 --- a/rest-client.ts +++ b/rest-client.ts @@ -1,9 +1,13 @@ -import axios, { AxiosInstance } from "axios"; +import axios, { AxiosInstance, AxiosAdapter } from "axios"; export class RestClient { private baseURL: string; private apiKey: string; + // Optional axios adapter injection (used by unit tests to capture the outgoing request without a + // real network call). Null in production, so behavior is unchanged — the x-api-key header is + // still set by initClient() below. + private adapter?: AxiosAdapter; private client : AxiosInstance | null = null; @@ -11,23 +15,25 @@ export class RestClient { return this.client != null ? this.client : this.initClient(); } - constructor(baseURL: string, apiKey: string) { + constructor(baseURL: string, apiKey: string, adapter?: AxiosAdapter) { this.baseURL = baseURL; this.apiKey = apiKey; + this.adapter = adapter; this.client = this.getClient(); } initClient() { return axios.create( { - baseURL: this.baseURL, + baseURL: this.baseURL, headers: { - "x-api-key": this.apiKey + "x-api-key": this.apiKey }, + ...(this.adapter ? { adapter: this.adapter } : {}), } ); } - + private errorHandling = (error: any) => { if(axios.isAxiosError(error)){ @@ -54,7 +60,7 @@ export class RestClient { }); return res.data; } catch (error) { - throw this.errorHandling(error); + throw this.errorHandling(error); } } @@ -70,7 +76,7 @@ export class RestClient { }); return res.data; } catch (error) { - throw this.errorHandling(error); + throw this.errorHandling(error); } } -} \ No newline at end of file +} diff --git a/scripts/Update-NodeClient.ps1 b/scripts/Update-NodeClient.ps1 new file mode 100644 index 0000000..6c0a8eb --- /dev/null +++ b/scripts/Update-NodeClient.ps1 @@ -0,0 +1,146 @@ +<# +.SYNOPSIS + Refreshes the committed CloudHub OpenAPI spec and verifies the hand-written Node client. + +.DESCRIPTION + Like the sibling PHP client (and unlike the Java client), this Node client is HAND-WRITTEN + TypeScript — there is NO code generator. This script does the two mechanical halves of a sync; + the human/AI edit in between is driven by the `update-cloudhub-node-client` skill. + + Pipeline stages: + 1. Build the CloudHub .NET Site (so its assemblies exist). + 2. Capture its OpenAPI spec headlessly -> openapi/cloudhub.json (Swashbuckle CLI). + => `git diff openapi/cloudhub.json` now shows exactly what changed upstream. + -- (manual) apply the spec changes to the *.ts sources, guided by the skill -- + 3. Verify: npm install, `tsc --noEmit` (typecheck), then `npm test` (build dist/ + node:test). + + Typical use: + pwsh scripts/Update-NodeClient.ps1 -SpecOnly # capture, then review the diff + pwsh scripts/Update-NodeClient.ps1 -SkipCloudHubBuild # after editing: re-capture + verify + pwsh scripts/Update-NodeClient.ps1 -VerifyOnly # just typecheck + test the current code + +.PARAMETER CloudHubRepo + Path to the CloudHub .NET repo. Defaults to a sibling folder named "cloudhub". + +.PARAMETER Configuration + .NET build configuration (Debug/Release). Default: Debug. + +.PARAMETER SpecOnly + Capture the spec only; skip verification. + +.PARAMETER SkipCloudHubBuild + Reuse already-built CloudHub assemblies (skip stage 1). + +.PARAMETER VerifyOnly + Skip build + capture; only typecheck + test the current TypeScript code. + +.PARAMETER SkipVerify + Capture the spec but skip the Node typecheck/test stage. + +.EXAMPLE + pwsh scripts/Update-NodeClient.ps1 -SpecOnly +.EXAMPLE + pwsh scripts/Update-NodeClient.ps1 -CloudHubRepo ..\cloudhub -Configuration Release +#> +[CmdletBinding()] +param( + [string] $CloudHubRepo, + [ValidateSet('Debug', 'Release')] [string] $Configuration = 'Debug', + [switch] $SpecOnly, + [switch] $SkipCloudHubBuild, + [switch] $VerifyOnly, + [switch] $SkipVerify +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan } +function Warn($msg) { Write-Host "WARNING: $msg" -ForegroundColor Yellow } +function Fail($msg) { Write-Error $msg; exit 1 } + +# --- Resolve paths ----------------------------------------------------------- +$RepoRoot = Split-Path -Parent $PSScriptRoot +Write-Step "Node client repo: $RepoRoot" +$SpecPath = Join-Path $RepoRoot 'openapi\cloudhub.json' + +# --- Capture the spec (stages 1-2), unless VerifyOnly ------------------------ +if (-not $VerifyOnly) { + if (-not $CloudHubRepo) { $CloudHubRepo = Join-Path (Split-Path -Parent $RepoRoot) 'cloudhub' } + $resolved = Resolve-Path -LiteralPath $CloudHubRepo -ErrorAction SilentlyContinue + $CloudHubRepo = if ($resolved) { $resolved.Path } else { $null } + if (-not $CloudHubRepo) { Fail "CloudHub repo not found. Pass -CloudHubRepo ." } + Write-Host "CloudHub repo: $CloudHubRepo" + + $SiteCsproj = Join-Path $CloudHubRepo 'Site\Lacuna.Cloudhub.Site.csproj' + if (-not (Test-Path $SiteCsproj)) { Fail "Site project not found at $SiteCsproj" } + + # Stage 1: build CloudHub + if (-not $SkipCloudHubBuild) { + Write-Step "Building CloudHub Site ($Configuration)" + dotnet build $SiteCsproj -c $Configuration --nologo -v minimal + if ($LASTEXITCODE -ne 0) { Fail "CloudHub build failed (exit $LASTEXITCODE)." } + } else { + Write-Step "Skipping CloudHub build (-SkipCloudHubBuild)" + } + + # Stage 2: capture the spec + Write-Step "Locating built Site assembly" + $SiteDll = Get-ChildItem -Path (Join-Path $CloudHubRepo "Site\bin\$Configuration") -Recurse -Filter 'Lacuna.Cloudhub.Site.dll' -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if (-not $SiteDll) { Fail "Could not find Lacuna.Cloudhub.Site.dll under Site\bin\$Configuration. Build first (omit -SkipCloudHubBuild)." } + Write-Host "Assembly: $($SiteDll.FullName)" + + Push-Location $RepoRoot + try { + Write-Step "Restoring the Swashbuckle CLI tool" + dotnet tool restore + if ($LASTEXITCODE -ne 0) { Fail "dotnet tool restore failed." } + + Write-Step "Capturing OpenAPI spec -> openapi/cloudhub.json" + New-Item -ItemType Directory -Force -Path (Split-Path $SpecPath) | Out-Null + dotnet swagger tofile --output $SpecPath $SiteDll.FullName v1 + if ($LASTEXITCODE -ne 0) { Fail "Spec capture failed. (Doc name 'v1' must match the Swashbuckle SwaggerDoc id in Site/Startup.cs.)" } + Write-Host "Spec written: $((Get-Item $SpecPath).Length) bytes" + } + finally { Pop-Location } + + if ($SpecOnly) { + Write-Step "Done (spec only). Review upstream changes with:" + Write-Host " git -C `"$RepoRoot`" diff -- openapi/cloudhub.json" + Write-Host "Then hand-apply the changes to the *.ts sources (see the update-cloudhub-node-client skill)." + exit 0 + } +} + +if ($SkipVerify) { Write-Step "Skipping verification (-SkipVerify)"; exit 0 } + +# --- Stage 3: verify the Node client ---------------------------------------- +Write-Step "Verifying the Node client" +$npm = Get-Command npm -ErrorAction SilentlyContinue +if (-not $npm) { + Warn "npm not found on PATH - skipping typecheck + tests. Install Node.js 18+ to enable verification." + exit 0 +} + +Push-Location $RepoRoot +try { + Write-Step "npm install" + & npm.cmd install --no-audit --no-fund + if ($LASTEXITCODE -ne 0) { Fail "npm install failed." } + + Write-Step "tsc --noEmit (typecheck)" + & npx.cmd tsc --noEmit + if ($LASTEXITCODE -ne 0) { Fail "TypeScript typecheck reported errors." } + + # `npm test` builds dist/ then runs the node:test suite (x-api-key header + string-enum wire format). + Write-Step "npm test (build dist/ + node:test)" + & npm.cmd test + if ($LASTEXITCODE -ne 0) { Fail "npm test reported failures." } +} +finally { Pop-Location } + +Write-Step "Done." +Write-Host "Review upstream API changes: git -C `"$RepoRoot`" diff -- openapi/cloudhub.json" +Write-Host "Review code changes: git -C `"$RepoRoot`" status" +Write-Host "Remember to bump `"version`" in package.json (and the README install snippet) - this package is versioned by package.json, not git tags." diff --git a/test/cloudhub.test.js b/test/cloudhub.test.js new file mode 100644 index 0000000..c9e9ca7 --- /dev/null +++ b/test/cloudhub.test.js @@ -0,0 +1,169 @@ +// CloudHub 2.0.0 wire-contract tests for the hand-written client. +// +// Uses Node's built-in test runner (node:test) — no external test framework. Instead of hitting the +// network, the tests inject a "capturing" axios adapter (RestClient's optional 3rd constructor arg, +// mirroring the PHP client's optional Guzzle handler) that records the outgoing request and returns +// a canned response. This lets us assert (a) the x-api-key header is sent, (b) `type` serializes as +// the STRING "SingleSignature", and (c) the 2.0.0 methods build the right URLs/bodies. +// +// Runs against the COMPILED output in ../dist, so `npm test` builds first. + +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const { + RestClient, + CloudHubClient, + TrustServiceSessionTypes, + IdentifierTypes, +} = require("../dist"); + +// Builds a capturing axios adapter. `calls` collects each outgoing request config; the adapter +// resolves with `responseData` as the response body. +function capturingAdapter(responseData) { + const calls = []; + const adapter = async (config) => { + calls.push(config); + return { + data: responseData, + status: 200, + statusText: "OK", + headers: {}, + config, + request: {}, + }; + }; + return { adapter, calls }; +} + +// axios v1 hands the adapter an AxiosHeaders instance; read a header case-insensitively. +function headerValue(headers, name) { + if (!headers) return undefined; + if (typeof headers.get === "function") { + const v = headers.get(name); + if (v != null) return v; + } + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === name.toLowerCase()) return headers[key]; + } + return undefined; +} + +test("RestClient sends the x-api-key header on GET", async () => { + const { adapter, calls } = capturingAdapter("\"BASE64CERT\""); + const rest = new RestClient("https://cloudhub.example/", "MY-API-KEY", adapter); + + const result = await rest.get("api/sessions/certificate?session=abc"); + + assert.equal(headerValue(calls[0].headers, "x-api-key"), "MY-API-KEY"); + // Default JSON decode is preserved: a byte-string endpoint resolves to a string. + assert.equal(result, "BASE64CERT"); +}); + +test("RestClient sends the x-api-key header on POST", async () => { + const { adapter, calls } = capturingAdapter({ services: [] }); + const rest = new RestClient("https://cloudhub.example/", "MY-API-KEY", adapter); + + await rest.post("api/sessions", { redirectUri: "https://localhost:3000/" }); + + assert.equal(headerValue(calls[0].headers, "x-api-key"), "MY-API-KEY"); +}); + +test("TrustServiceSessionTypes / IdentifierTypes members are string-valued", () => { + // CloudHub 2.0.0 breaking change: these were integers in 1.x. + assert.equal(TrustServiceSessionTypes.SingleSignature, "SingleSignature"); + assert.equal(TrustServiceSessionTypes.MultiSignature, "MultiSignature"); + assert.equal(TrustServiceSessionTypes.SignatureSession, "SignatureSession"); + assert.equal(TrustServiceSessionTypes.AuthenticationSession, "AuthenticationSession"); + assert.equal(IdentifierTypes.CPF, "CPF"); + assert.equal(IdentifierTypes.CNPJ, "CNPJ"); +}); + +test("session type serializes as the string \"SingleSignature\" on the wire", async () => { + const { adapter, calls } = capturingAdapter({ services: [] }); + const client = new CloudHubClient("https://cloudhub.example/", "K", adapter); + + await client.createSessionAsync({ + identifier: "12345678909", + redirectUri: "https://localhost:3000/", + type: TrustServiceSessionTypes.SingleSignature, + }); + + assert.equal(calls[0].url, "api/sessions"); + const sentBody = JSON.parse(calls[0].data); + assert.equal(sentBody.type, "SingleSignature"); // string, not integer 1 + assert.equal(sentBody.redirectUri, "https://localhost:3000/"); +}); + +test("getServiceAvailabilityAsync builds the path + query (2.0.0)", async () => { + const { adapter, calls } = capturingAdapter({ discoveryAvailable: true, certificateFound: false }); + const client = new CloudHubClient("https://cloudhub.example/", "K", adapter); + + const res = await client.getServiceAvailabilityAsync("safeid", "12345678909", IdentifierTypes.CPF); + + assert.equal(calls[0].url, "api/sessions/services/safeid/availability?identifier=12345678909&identifierType=CPF"); + assert.equal(res.discoveryAvailable, true); + assert.equal(res.certificateFound, false); +}); + +test("getServiceAvailabilityAsync omits the query when no identifier is given", async () => { + const { adapter, calls } = capturingAdapter({ discoveryAvailable: false }); + const client = new CloudHubClient("https://cloudhub.example/", "K", adapter); + + await client.getServiceAvailabilityAsync("safeid"); + + assert.equal(calls[0].url, "api/sessions/services/safeid/availability"); +}); + +test("createServiceSessionAsync posts to the named-service path with no CPF (2.0.0)", async () => { + const { adapter, calls } = capturingAdapter({ + serviceInfo: { serviceName: "safeid", provider: "Safeweb", endpoint: "https://s/", badgeUrl: null }, + authUrl: "https://safeid.example/auth?qr=1", + }); + const client = new CloudHubClient("https://cloudhub.example/", "K", adapter); + + const res = await client.createServiceSessionAsync("safeid", { + redirectUri: "https://localhost:3000/", + type: TrustServiceSessionTypes.SingleSignature, + }); + + assert.equal(calls[0].url, "api/sessions/services/safeid"); + const sentBody = JSON.parse(calls[0].data); + assert.equal(sentBody.redirectUri, "https://localhost:3000/"); + assert.equal(sentBody.type, "SingleSignature"); + assert.equal("identifier" in sentBody, false); // no CPF sent + assert.equal(res.authUrl, "https://safeid.example/auth?qr=1"); + assert.equal(res.serviceInfo.badgeUrl, null); // nullable badgeUrl must not choke anything +}); + +test("getCertificateModelAsync hits the v2 endpoint and returns serviceName (2.0.0)", async () => { + const { adapter, calls } = capturingAdapter({ content: "AQID", alias: "my-cert", serviceName: "BirdID" }); + const client = new CloudHubClient("https://cloudhub.example/", "K", adapter); + + const res = await client.getCertificateModelAsync("sess+1/2=="); + + assert.equal(calls[0].url, "api/v2/sessions/certificate?session=" + encodeURIComponent("sess+1/2==")); + assert.equal(res.serviceName, "BirdID"); // serviceName is a 2.0.0 addition + assert.equal(res.content, "AQID"); +}); + +test("getCustomStateAsync hits the custom-state endpoint (2.0.0)", async () => { + const { adapter, calls } = capturingAdapter("my-client-state"); + const client = new CloudHubClient("https://cloudhub.example/", "K", adapter); + + const res = await client.getCustomStateAsync("abc"); + + assert.equal(calls[0].url, "api/sessions/custom-state?session=abc"); + assert.equal(res, "my-client-state"); +}); + +test("getCertificateAsync (v1) still resolves to a base64 string — backward compatible", async () => { + const { adapter, calls } = capturingAdapter("\"BASE64CERT\""); + const client = new CloudHubClient("https://cloudhub.example/", "K", adapter); + + const cert = await client.getCertificateAsync("abc"); + + assert.equal(calls[0].url, "api/sessions/certificate?session=abc"); + assert.equal(typeof cert, "string"); + assert.equal(cert, "BASE64CERT"); +}); diff --git a/types.js b/types.js deleted file mode 100644 index 5877605..0000000 --- a/types.js +++ /dev/null @@ -1,10 +0,0 @@ -"use strict"; -exports.__esModule = true; -exports.TrustServiceSessionTypes = void 0; -var TrustServiceSessionTypes; -(function (TrustServiceSessionTypes) { - TrustServiceSessionTypes[TrustServiceSessionTypes["SingleSignature"] = 1] = "SingleSignature"; - TrustServiceSessionTypes[TrustServiceSessionTypes["MultiSignature"] = 2] = "MultiSignature"; - TrustServiceSessionTypes[TrustServiceSessionTypes["SignatureSession"] = 3] = "SignatureSession"; - TrustServiceSessionTypes[TrustServiceSessionTypes["AuthenticationSession"] = 4] = "AuthenticationSession"; -})(TrustServiceSessionTypes = exports.TrustServiceSessionTypes || (exports.TrustServiceSessionTypes = {})); diff --git a/types.ts b/types.ts index 8d0984f..7b5ba74 100644 --- a/types.ts +++ b/types.ts @@ -1,39 +1,111 @@ -// Types.ts +// types.ts — CloudHub client data contracts. +// +// These are kept as interfaces (not classes) because consumers pass plain object literals to the +// client, e.g. createSessionAsync({ identifier, redirectUri, type: TrustServiceSessionTypes.SingleSignature }) +// (see the PkiSuiteSamples nodejs routes). Enums stay as `enum` because callers reference the +// members by name (TrustServiceSessionTypes.SingleSignature). + +/** + * CloudHub 2.0.0: session types are STRING-valued on the wire (they were integers in 1.x). This is + * the breaking change behind the 2.0 major bump. Callers reference the members by name + * (TrustServiceSessionTypes.SingleSignature), so their code is unchanged — only the emitted value + * changes from `1` to `"SingleSignature"`. + */ +export enum TrustServiceSessionTypes { + SingleSignature = "SingleSignature", + MultiSignature = "MultiSignature", + SignatureSession = "SignatureSession", + AuthenticationSession = "AuthenticationSession", +} + +/** + * CloudHub 2.0.0: signer identifier kind. Used by SessionCreateRequest.identifierType and the + * services/availability query. String-valued on the wire. + */ +export enum IdentifierTypes { + CPF = "CPF", + CNPJ = "CNPJ", +} + export interface SessionCreateRequest { identifier?: string; + /** Required by the server (a session must have somewhere to redirect back to). */ redirectUri?: string; type?: TrustServiceSessionTypes; - lifetimeInSeconds: number; + /** Optional; a missing value means "no explicit lifetime" (the server applies its own default). */ + lifetimeInSeconds?: number; + // CloudHub 2.0.0 additions: + identifierType?: IdentifierTypes; + customState?: string; + discover?: boolean; } -export enum TrustServiceSessionTypes { - SingleSignature = 1, - MultiSignature, - SignatureSession, - AuthenticationSession, +/** + * CloudHub 2.0.0: request body of POST /api/sessions/services/{name} (create a session against a + * single, named trust service). Mirrors SessionCreateRequest WITHOUT `identifierType` — the service + * is named in the URL path instead. `identifier` is optional: omit it to start a service session + * with no CPF/CNPJ (the provider identifies the signer during its own auth flow, e.g. a QR code). + */ +export interface ServiceSessionCreateRequest { + /** Required by the server. */ + redirectUri?: string; + type?: TrustServiceSessionTypes; + identifier?: string; + lifetimeInSeconds?: number; + customState?: string; + discover?: boolean; } export interface SessionModel { + // The 2.0.0 spec marks `services` nullable, so the server may return null or omit it — callers + // must not assume the array is present. services?: Array | null; } export interface TrustServiceAuthParametersModel { serviceInfo?: TrustServiceInfoModel; - authUrl?: string; + // 2.0.0 spec marks `authUrl` nullable. + authUrl?: string | null; } +/** + * CloudHub 2.0.0: response of POST /api/sessions/services/{name}. Structurally identical to + * TrustServiceAuthParametersModel (the server declares it as a subclass of it). + */ +export type ServiceSessionCreateResponse = TrustServiceAuthParametersModel; + export interface TrustServiceInfoModel { - serviceName: string; - provider: string; - endpoint: string; - badgeUrl: string; + // The 2.0.0 spec marks all four fields nullable. `badgeUrl` in particular is commonly null (the + // PkiSuiteSamples discover page renders an "Empty BadgeUrl" fallback), so none may be assumed set. + serviceName?: string | null; + provider?: string | null; + endpoint?: string | null; + badgeUrl?: string | null; } -export interface SignHashRequest -{ +export interface SignHashRequest { session: string; hash: string; digestAlgorithm?: string; digestAlgorithmOid?: string; certificateAlias?: string; } + +/** + * CloudHub 2.0.0: response of GET /api/v2/sessions/certificate. `serviceName` was added in 2.0. The + * v1 getCertificateAsync() still returns the raw base64 certificate string for backward compatibility. + */ +export interface CertificateModel { + /** Base64-encoded certificate bytes. */ + content?: string | null; + alias?: string | null; + serviceName?: string | null; +} + +/** + * CloudHub 2.0.0: response of GET /api/sessions/services/{name}/availability. + */ +export interface GetServiceAvailabilityResponse { + discoveryAvailable: boolean; + certificateFound?: boolean | null; +}