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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .env.live.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Runnable examples default to PAT. Set sa explicitly to use SA Key or SAT.
QODER_AUTH_MODE=pat
# For SA examples, selects the business and exchange hosts together.
QODER_SA_REGION=cn

# Dedicated test account. Never commit credentials.
QODER_FORWARD_BASE_URL=https://api.qoder.com/api/v1/forward
QODER_FORWARD_PAT=
Expand All @@ -18,3 +23,17 @@ QODER_MANAGED_LIVE_TIMEOUT_SECONDS=15

# Execution wait and each independent cleanup operation deadline.
QODER_E2E_TIMEOUT_SECONDS=180

# Separate SA tests run CN and Global; they load this file or process variables.
# QODER_AUTH_MODE selects examples only; ordinary PAT suites stay PAT.
# Never commit real keys.
# Ordinary clients do not read these implicitly.
QODER_SA_CN_KEY=
QODER_SA_GLOBAL_KEY=
# Separate SA execution entry; set both flags only for dedicated test accounts.
QODER_SA_LIVE_ALLOW_WRITE=false
QODER_SA_LIVE_ALLOW_EXECUTION=false
QODER_SA_MODEL=auto
# Optional single-region factory input (QODER_SA_KEY), used only explicitly.
QODER_SA_KEY=
QODER_ACCESS_TOKEN=
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ existing `0.1.0` release; earlier development prereleases are not listed.

## [Unreleased]

### Added

- Read-only SA Key and SAT authentication examples, plus explicit PAT/SA selection for existing business examples without duplicating scenarios.
- Shared PAT and Service Account authentication for Forward and Managed, including explicit SA mode, direct SAT access, and lazy SA Key exchange with an instance-owned cache, early renewal, concurrent request sharing, and failure cooldown.
- Separate CN/Global SA authentication smoke checks for both API surfaces, covering automatic exchange and direct SAT use, plus opt-in write/read, Session SSE and cleanup verification.

### Changed

- Support `QODER_ACCESS_TOKEN` for the selected mode. SA never falls back to `QODER_PAT`; ordinary clients never implicitly read `QODER_SA_KEY`.
- Add `accessToken` and `authMode`; client copies retain authentication mode and source. Legacy `pat` plus a custom credential retains provider precedence.

### Migration

- For an existing SAT, select SA mode and supply the neutral token option. For automatic exchange, construct a Service Account credential with explicit region and scopes; do not combine it with a static token. Existing PAT calls require no new mode argument.

### Fixed

- Example failure logs retain error names, HTTP status, error codes and request IDs without dumping authenticated request objects.

## [0.3.0]

### Added
Expand Down
36 changes: 35 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ npm run docs:check
- **Unit and contract tests** live under `tests/` and run with `npm test`; focused entries include `npm run test:contract` and `npm run test:scenarios`.
- **Integration tests** live under `tests/live/`. Copy `.env.live.example` to `.env.live`, use a dedicated test account, enable only the required write/execution gates, then run `npm run test:live`.
- **E2E tests** execute real models and tools and run with `npm run test:e2e` after all documented execution gates are enabled.
- **Examples** are runnable documentation. Tests must not import from `examples/`; build first and run examples directly as documented in the README.
- **Examples** are runnable documentation. Business integration tests must not import from `examples/`; build first and run examples directly as documented in the README.

Never commit `.env.live`, tokens, credentials, generated logs, or test output. Live scenarios must register cleanup immediately after creating a resource.

Expand Down Expand Up @@ -144,3 +144,37 @@ If a workflow-only fix is needed after the release tag was created, merge that f
## Pull requests

Complete the pull request template, include exact verification commands and results, and identify public API, documentation, integration-test, and cross-SDK effects. Do not combine unrelated refactors with behavior changes.

## Service Account authentication checks

The separate read-only SA suite requires `QODER_SA_CN_KEY` and
`QODER_SA_GLOBAL_KEY` in `.env.live` or the process environment and provisioned SA permissions
(and Managed workspace bindings). Run `npm run test:live:sa`. It tests both API
surfaces in CN and Global using automatic exchange and static SAT. Required keys
are checked before the first API call; normal PAT suites remain independent.
The read-only command does not prove write, model execution, SSE, or
user/identity isolation coverage. Do not print
SA Keys or SATs in test reports.

For write/read, Session SSE and cleanup verification, use dedicated SA accounts
and run `npm run test:live:sa:execution` with
`QODER_SA_LIVE_ALLOW_WRITE=true` and `QODER_SA_LIVE_ALLOW_EXECUTION=true`.
`QODER_SA_MODEL` selects the model (default `auto`). This separate entry creates
unique test Environments, Templates/Agents, Forward Identities and Sessions in
both regions, sends one message per Session, checks the assistant reply and idle
event through SSE, and cleans up resources even after execution fails. Cleanup
failures fail the test.
Missing keys or execution flags fail this explicit command before resource creation.
The read-only SA command does not run these execution cases.

Process environment variables override `.env.live`; explicit empty values do not
fall back to the file. Use `LIVE_ENV_FILE` to select another file.
`QODER_AUTH_MODE` selects example authentication only. PAT and SA test entries
are separate; setting it to `sa` does not switch the ordinary PAT suites.

`npm test` includes SA live-file syntax checks, configuration regression tests,
and CJS/ESM public export checks without credentials or network calls.

Business integration scenarios keep independent test helpers. Dedicated offline
regression tests for example configuration and lifecycle may import examples;
those tests do not replace SDK integration coverage.
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ remains available.

### Configuration

`pat` accepts a string or a function returning a string or a promise, which is re-resolved before every request attempt — useful for short-lived tokens. A `Credential` object can be supplied instead; `PATCredential.fromEnv()` reads `QODER_PAT`.
`accessToken` accepts a string or a function returning a string or a promise, re-resolved before every request attempt. `pat` remains available for existing PAT integrations. `QODER_ACCESS_TOKEN` supplies the selected mode's token; PAT mode falls back to `QODER_PAT`. A `Credential` object can be supplied instead; see the authentication section for automatic SA exchange.

The base URL comes from `QODER_FORWARD_BASE_URL` for Forward and `QODER_BASE_URL` for Managed, defaulting to `https://api.qoder.com/api/v1/forward` and `https://api.qoder.com/api/v1/cloud`. Explicit constructor options always win.

Expand All @@ -126,6 +126,91 @@ const client = new ManagedClient({
});
```

## PAT and Service Account authentication

PAT and SA are parallel modes. Existing `pat` calls keep working. For an existing
SAT use `{ authMode: 'sa', accessToken }`. `accessToken` also accepts a callback.
`QODER_ACCESS_TOKEN` supplies the selected mode's token; only PAT mode falls back
to `QODER_PAT`. Ordinary clients never automatically read `QODER_SA_KEY`.

For automatic SAT exchange, explicitly construct a shared credential:

```ts
import { ForwardClient, ManagedClient, ServiceAccountCredential } from 'qca-sdk';

const credential = ServiceAccountCredential.fromEnv({
region: 'global',
scopes: ['qca.access', 'forward.access'],
});
const forward = new ForwardClient({ credential });
const managed = new ManagedClient({ credential });
```

For CN, use `region: 'cn'` and matching `https://api.qoder.com.cn/api/v1/forward/`
and `https://api.qoder.com.cn/api/v1/cloud/` BaseURLs. Managed-only integrations
can request `['qca.access']`. Scopes and Service Account permissions/workspace
bindings must be configured server-side; the SDK does not provision them.

The credential lazily exchanges the SA Key, caches the server's actual lifetime,
and renews on the next request in the refresh window. Concurrent callers share
one exchange. A transient failure can reuse a still-safe SAT; authentication or
invalid-response failures cannot. Failures enter a bounded cooldown. There is
no PAT fallback, background refresh, or automatic replay of a 401 write request.
The independent exchange timeout includes reading its body (default 30 seconds).
Cancelling a business request ends that caller's wait; the shared exchange can
continue and cache its result. Already-open streams are not closed by cache expiry.

Do not combine `accessToken` with `pat` or `credential`, or an SA credential with
a static token. Legacy `pat` + custom credential retains provider precedence.
Request Authorization headers still override automatic injection. `withOptions`
retains mode and source; explicitly changing `authMode` clears the other mode's
credentials before applying new options.

Run the read-only CN/Global SA smoke separately with `npm run test:live:sa`, setting
`QODER_SA_CN_KEY` and `QODER_SA_GLOBAL_KEY` in `.env.live` or the process environment.
Process variables take precedence; `LIVE_ENV_FILE` selects another file.
Both keys are checked before the first API call. For write/read, Session SSE and
cleanup verification, run `npm run test:live:sa:execution` with
`QODER_SA_LIVE_ALLOW_WRITE=true` and `QODER_SA_LIVE_ALLOW_EXECUTION=true`.
`QODER_SA_MODEL` defaults to `auto`. Ordinary offline and PAT suites do not require
SA credentials; `QODER_AUTH_MODE` switches examples, not the PAT test suites. See the
[official authentication contract](https://docs.qoder.com/cloud-agents/api/conventions/authentication).

### Authentication examples

The two read-only examples under `examples/auth/` show client construction directly:
SA Key exchange with automatic caching/renewal, and direct SAT access with renewal
managed by the caller. Each lists Forward templates and Managed agents; no model
execution or resources are created. The SA needs both API scopes/permissions and
its Managed workspace binding in the selected region.

```sh
npm run build
# Configure QODER_SA_REGION=cn|global and only the credential needed by the demo.
node --env-file=.env.live examples/auth/sa-key.mjs # QODER_SA_KEY; leave QODER_ACCESS_TOKEN empty
node --env-file=.env.live examples/auth/sa-token.mjs # QODER_ACCESS_TOKEN; leave QODER_SA_KEY empty
```

The existing business examples keep PAT as the default. Node's `--env-file` can
load `.env.live`; the example helpers themselves read process environment variables.
Set `QODER_AUTH_MODE=sa` to select SA explicitly, then configure exactly one of
`QODER_SA_KEY` and `QODER_ACCESS_TOKEN`. `QODER_SA_REGION=cn|global` selects both the
business and exchange hosts. Leave the unused credential empty. Ambient PATs are
ignored in SA mode; missing SA credentials and conflicting Key/SAT inputs fail
before a request is sent. This selection is example configuration, not implicit
SDK behavior.

```sh
# Reuse the same models or Session program with SA authentication.
QODER_AUTH_MODE=sa QODER_SA_REGION=cn node --env-file=.env.live examples/forward/models.mjs
QODER_AUTH_MODE=sa QODER_SA_REGION=global node --env-file=.env.live examples/managed/session.mjs
```

For a centralized SAT service, use
`new ForwardClient({ authMode: 'sa', accessToken: () => tokenManager.getValidSAT() })`.
The callback may return a promise and is called before each HTTP attempt; the token
manager is responsible for renewing the SAT.

## Request and response types

Every operation is typed. The two APIs define independent type sets, so request and response types are exported from the subpath entry points rather than the package root:
Expand Down
18 changes: 18 additions & 0 deletions docs/api/forward/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ Re-exports [AuthenticationError](../index/classes/AuthenticationError.md)

***

### AuthMode

Re-exports [AuthMode](../index/type-aliases/AuthMode.md)

***

### BadRequestError

Re-exports [BadRequestError](../index/classes/BadRequestError.md)
Expand Down Expand Up @@ -381,6 +387,18 @@ Re-exports [ResumableSessionEventStream](../index/classes/ResumableSessionEventS

***

### ServiceAccountCredential

Re-exports [ServiceAccountCredential](../index/classes/ServiceAccountCredential.md)

***

### ServiceAccountCredentialOptions

Re-exports [ServiceAccountCredentialOptions](../index/interfaces/ServiceAccountCredentialOptions.md)

***

### Stream

Re-exports [Stream](../index/classes/Stream.md)
Expand Down
3 changes: 3 additions & 0 deletions docs/api/index/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- [QoderError](classes/QoderError.md)
- [RateLimitError](classes/RateLimitError.md)
- [ResumableSessionEventStream](classes/ResumableSessionEventStream.md)
- [ServiceAccountCredential](classes/ServiceAccountCredential.md)
- [Stream](classes/Stream.md)
- [UnprocessableEntityError](classes/UnprocessableEntityError.md)

Expand All @@ -39,9 +40,11 @@
- [PageResponse](interfaces/PageResponse.md)
- [RequestOptions](interfaces/RequestOptions.md)
- [ServerSentEvent](interfaces/ServerSentEvent.md)
- [ServiceAccountCredentialOptions](interfaces/ServiceAccountCredentialOptions.md)

## Type Aliases

- [AuthMode](type-aliases/AuthMode.md)
- [HeadersLike](type-aliases/HeadersLike.md)
- [Middleware](type-aliases/Middleware.md)
- [PaginationMode](type-aliases/PaginationMode.md)
Expand Down
6 changes: 6 additions & 0 deletions docs/api/index/classes/APIClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@

## Properties

### authMode

> `readonly` **authMode**: [`AuthMode`](../type-aliases/AuthMode.md)

***

### baseURL

> `readonly` **baseURL**: `string`
Expand Down
10 changes: 10 additions & 0 deletions docs/api/index/classes/ForwardClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@

## Properties

### authMode

> `readonly` **authMode**: [`AuthMode`](../type-aliases/AuthMode.md)

#### Inherited from

[`APIClient`](APIClient.md).[`authMode`](APIClient.md#authmode)

***

### baseURL

> `readonly` **baseURL**: `string`
Expand Down
10 changes: 10 additions & 0 deletions docs/api/index/classes/ManagedClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ Qoder Managed Mode API client.

***

### authMode

> `readonly` **authMode**: [`AuthMode`](../type-aliases/AuthMode.md)

#### Inherited from

[`APIClient`](APIClient.md).[`authMode`](APIClient.md#authmode)

***

### baseURL

> `readonly` **baseURL**: `string`
Expand Down
81 changes: 81 additions & 0 deletions docs/api/index/classes/ServiceAccountCredential.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
[**qca-sdk**](../../README.md)

***

[qca-sdk](../../README.md) / [index](../README.md) / ServiceAccountCredential

# Class: ServiceAccountCredential

Lazy, instance-owned SAT cache. Share an instance between clients in the same region.

## Implements

- [`Credential`](../interfaces/Credential.md)

## Constructors

### Constructor

> **new ServiceAccountCredential**(`options`): `ServiceAccountCredential`

#### Parameters

##### options

[`ServiceAccountCredentialOptions`](../interfaces/ServiceAccountCredentialOptions.md)

#### Returns

`ServiceAccountCredential`

## Properties

### region

> `readonly` **region**: `"cn"` \| `"global"`

## Methods

### getToken()

> **getToken**(): `Promise`\<`string`\>

#### Returns

`Promise`\<`string`\>

#### Implementation of

[`Credential`](../interfaces/Credential.md).[`getToken`](../interfaces/Credential.md#gettoken)

***

### validateRegion()

> **validateRegion**(`host`): `void`

#### Parameters

##### host

`string`

#### Returns

`void`

***

### fromEnv()

> `static` **fromEnv**(`options`): `ServiceAccountCredential`

#### Parameters

##### options

`Omit`\<[`ServiceAccountCredentialOptions`](../interfaces/ServiceAccountCredentialOptions.md), `"saKey"`\>

#### Returns

`ServiceAccountCredential`
Loading
Loading