@orvello/sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,16 @@ accounting, invoicing, bookkeeping, customer-company and payments platform.
15
15
  - Native PDF and binary blob streaming (`Blob`).
16
16
  - `Idempotency-Key` support for safe webhooks and financial mutations.
17
17
  - **Zero Runtime Dependencies**: Uses native Fetch and WebCrypto.
18
+ - **The route picks the credential**: the contract carries `auth.type` per route, so business
19
+ routes go out as `Authorization: Bearer` and management routes as `x-api-key` — from the same
20
+ client, without the caller choosing.
21
+ - **Preflight instead of guessing**: `preflight(operationId)` walks the entire path a real call
22
+ would take and stops before writing, so an edge can be re-verified at any time without touching
23
+ it.
24
+ - **Capabilities as data**: `capabilities()` reports what this service can do, derived from the
25
+ contract and backed by the operations behind each claim.
26
+ - **Circuit breaker**: opens on transport failures, deliberately not on 401/403 — a rejected
27
+ credential is not an outage.
18
28
  - **Flexible Auth**: Supports static API Keys, Bearer tokens, async token getters, and OAuth2
19
29
  Client Credentials with automatic token caching.
20
30
  - **Ergonomic Accounting Helpers**: Bank account caching (`getDefaultBankAccount`), base64 receipt
@@ -72,6 +82,10 @@ ORVELLO_OAUTH_CLIENT_SECRET=...
72
82
  ORVELLO_OAUTH_SCOPE=...
73
83
  # or instead of OAuth:
74
84
  ORVELLO_API_KEY=...
85
+
86
+ # The management credential for /oauth/clients/external/* — a different value,
87
+ # not another spelling of the one above.
88
+ ORVELLO_MANAGEMENT_API_KEY=...
75
89
  ```
76
90
 
77
91
  ```typescript
@@ -84,6 +98,143 @@ silently pointing at a fixed host.
84
98
 
85
99
  ---
86
100
 
101
+ ## Two credentials, and the route decides which one goes out
102
+
103
+ orvello guards its endpoints in two different ways, and they are not
104
+ interchangeable:
105
+
106
+ | | Business routes | Management routes |
107
+ |---|---|---|
108
+ | Examples | `/invoices/external`, `/customer-companies/external`, `/bookkeeping/*` | `/oauth/clients/external/*` |
109
+ | Header | `Authorization: Bearer …` | `x-api-key: …` |
110
+ | Credential | an OAuth2 client (client_credentials) or a token | the service's management key |
111
+ | Belongs to | the **calling app** | the **edge** between manager and service |
112
+ | Configure with | `auth` / `ORVELLO_OAUTH_CLIENT_ID` + `ORVELLO_OAUTH_CLIENT_SECRET` | `managementApiKey` / `ORVELLO_MANAGEMENT_API_KEY` |
113
+
114
+ You never pick. The generated contract carries `auth.type` on every single route,
115
+ and the client reads it — so `client.invoices.getPending()` goes out as a bearer
116
+ call and `client.oauthClients.create(…)` goes out with `x-api-key`, from the same
117
+ client instance. A hand-built path through `client.request()` gets the same
118
+ treatment, because it looks the route up too.
119
+
120
+ ```ts
121
+ const orvello = createOrvelloClient({
122
+ baseUrl: () => resolveLiveBaseUrl("node-bill"),
123
+ auth: { clientId, clientSecret, scope: "invoices:read invoices:write" },
124
+ managementApiKey: process.env.ORVELLO_MANAGEMENT_API_KEY,
125
+ });
126
+
127
+ await orvello.invoices.getPending(); // Bearer
128
+ await orvello.oauthClients.create({ name: "shop", role: "editor" }); // x-api-key
129
+ ```
130
+
131
+ Leaving `managementApiKey` out is a valid choice: a client that only writes
132
+ invoices cannot then hand out access. Calling a management route without it
133
+ fails **before** anything goes on the wire, with a message that names the missing
134
+ setting — rather than earning a `403 Forbidden: Invalid API key`, which reads
135
+ like a wrong key instead of an absent one.
136
+
137
+ ## Calling anything in the contract
138
+
139
+ The typed namespaces cover the endpoints most integrations need. Everything else
140
+ is reachable by its `operationId`, with method, path and credential taken from
141
+ the contract:
142
+
143
+ ```ts
144
+ const clients = await orvello.call("oauth2_external_clients_list", {
145
+ query: { pageSize: 50, isActive: true },
146
+ });
147
+
148
+ const rotated = await orvello.call("oauth2_external_clients_rotate", {
149
+ params: { idOrClientId: "nbill_oauth2_3Rxp…" },
150
+ body: { gracePeriodHours: 2 },
151
+ });
152
+ ```
153
+
154
+ An unknown `operationId` throws immediately, and so does a missing path
155
+ parameter — instead of sending `/oauth/clients/external/:idOrClientId` to the
156
+ service and getting a puzzling 404 back.
157
+
158
+ ## Preflight: would this call work, without making it
159
+
160
+ Creating an OAuth2 client writes real data on both sides. That is a deliberate,
161
+ approved step — but you still want to know, at any time and without an occasion,
162
+ whether the path still holds.
163
+
164
+ `preflight()` walks the whole way an actual call would take — same base URL, same
165
+ credential channel, same contract — and stops before writing:
166
+
167
+ ```ts
168
+ const report = await orvello.preflight("oauth2_external_clients_create", {
169
+ body: { name: "node-shop invoicing", role: "editor", scopes: ["invoices:write"] },
170
+ });
171
+
172
+ report.ok; // true → the path holds today
173
+ report.sideEffects; // "none" — always; a preflight never writes
174
+ report.authChannel; // "api_key"
175
+ report.steps; // operation → path-parameters → request-shape →
176
+ // reachable → contract → credential-configured → credential-accepted
177
+ ```
178
+
179
+ Every step carries its reason in plain words, and a skipped step is reported as
180
+ skipped — never as passed. What a green report proves is **entitlement**, not
181
+ **outcome**: the service accepts your credential for this route. It does not
182
+ predict what the write would return. `capabilities().capabilities.serverSideDryRun`
183
+ says so explicitly, because orvello offers no server-side rehearsal for client
184
+ management.
185
+
186
+ Use `skipCredentialProbe: true` to check many edges cheaply — it then only asks
187
+ whether a credential is configured, and says so.
188
+
189
+ ## Capabilities: what this service can do, as data
190
+
191
+ A connection manager can only establish, verify and repair an edge if it knows
192
+ what the other side actually offers. `capabilities()` answers that from the
193
+ bundled contract — no network, no credential, so it also works while the
194
+ connection is down:
195
+
196
+ ```ts
197
+ const report = orvello.capabilities();
198
+
199
+ report.contractSha256; // which contract this build speaks
200
+ report.operationCount; // how many operations it knows
201
+ report.authChannels; // { bearer, api_key, none }
202
+ report.capabilities.clientManagement.supported; // true
203
+ report.capabilities.clientManagement.operations; // the operations that back the claim
204
+ report.capabilities.rotationGracePeriod.supported; // retire + rollback exist
205
+ report.capabilities.serverSideDryRun.reason; // why it is false
206
+ ```
207
+
208
+ A capability counts as present exactly when the operations behind it are in the
209
+ contract, and every entry lists them — so a claim can be checked instead of
210
+ believed. Drop a route in orvello and it disappears here on the next
211
+ `sync:contract`, with nobody having to remember.
212
+
213
+ ## Health and the circuit breaker
214
+
215
+ ```ts
216
+ const health = await orvello.checkHealth();
217
+ health.available; // does the service answer at all
218
+ health.authenticated; // was a credential actually accepted — null means "not asked"
219
+
220
+ orvello.isAvailable(); // circuit-breaker state, no network
221
+ orvello.circuitState();
222
+ orvello.resetCircuit();
223
+ ```
224
+
225
+ `available` and `authenticated` are two questions and stay apart. `authenticated:
226
+ null` means no credential was configured, never "fine".
227
+
228
+ The breaker opens after repeated **transport** failures — timeouts, refused
229
+ connections, 5xx — and short-circuits further calls for a cooldown instead of
230
+ letting each one burn its full timeout. It deliberately does **not** open on 401
231
+ or 403: the service answered, it just said no. A breaker that trips on permission
232
+ errors turns a wrong credential into an apparent outage and sends the search in
233
+ the wrong direction. Configure with `circuitBreaker: { threshold, cooldownMs }`,
234
+ or switch it off with `circuitBreaker: false`.
235
+
236
+ ---
237
+
87
238
  ## Usage Examples
88
239
 
89
240
  ### 1. Invoices