@agentforge-io/sdk-console 0.0.0-stage → 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 +172 -2
- package/dist/client.d.ts +43 -0
- package/dist/client.js +58 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +93 -0
- package/dist/http.d.ts +76 -0
- package/dist/http.js +289 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +57 -0
- package/dist/oauth.d.ts +37 -0
- package/dist/oauth.js +97 -0
- package/dist/resources/agents.d.ts +40 -0
- package/dist/resources/agents.js +67 -0
- package/dist/resources/apps.d.ts +16 -0
- package/dist/resources/apps.js +26 -0
- package/dist/resources/auth.d.ts +50 -0
- package/dist/resources/auth.js +87 -0
- package/dist/resources/connectors.d.ts +25 -0
- package/dist/resources/connectors.js +47 -0
- package/dist/resources/marketplace.d.ts +10 -0
- package/dist/resources/marketplace.js +14 -0
- package/dist/resources/members.d.ts +62 -0
- package/dist/resources/members.js +85 -0
- package/dist/resources/playground.d.ts +22 -0
- package/dist/resources/playground.js +32 -0
- package/dist/resources/teams.d.ts +49 -0
- package/dist/resources/teams.js +78 -0
- package/dist/resources/templates.d.ts +115 -0
- package/dist/resources/templates.js +187 -0
- package/dist/resources/tenants.d.ts +12 -0
- package/dist/resources/tenants.js +25 -0
- package/dist/resources/tools.d.ts +13 -0
- package/dist/resources/tools.js +17 -0
- package/dist/resources/variables.d.ts +28 -0
- package/dist/resources/variables.js +43 -0
- package/dist/token-store.d.ts +23 -0
- package/dist/token-store.js +27 -0
- package/dist/types.d.ts +1009 -0
- package/dist/types.js +19 -0
- package/package.json +44 -4
package/README.md
CHANGED
|
@@ -1,3 +1,173 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agentforge-io/sdk-console
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Session client for **admin apps** of the AgentForge platform: the Poodleck Console
|
|
4
|
+
(Expo: iOS, Android, web), or any client that signs in as an AgentForge **user**
|
|
5
|
+
(dashboard JWT) rather than as an App (`@agentforge-io/sdk-server`, `sk_live_`).
|
|
6
|
+
|
|
7
|
+
- Injectable `fetch`: runs in React Native, browsers and Node. No Node-only APIs in
|
|
8
|
+
the runtime code (no Buffer / crypto / fs).
|
|
9
|
+
- `TokenStore` adapter: you decide where the tokens live (SecureStore, web storage, memory).
|
|
10
|
+
- Automatic **single-flight refresh** on 401, then one retry. A burst of parallel 401s
|
|
11
|
+
triggers exactly one `POST /auth/refresh` (the refresh token rotates on use).
|
|
12
|
+
- Typed `ApiError` (`status`, `code`, `message`, `details`) and an `onSignedOut` callback.
|
|
13
|
+
- `forTenant(tenantId)` binds every `admin/tenants/:tenantId/...` resource to a workspace.
|
|
14
|
+
|
|
15
|
+
## Usage (Expo / React Native)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import * as SecureStore from 'expo-secure-store';
|
|
19
|
+
import { ConsoleClient, ApiError, type TokenStore } from '@agentforge-io/sdk-console';
|
|
20
|
+
|
|
21
|
+
const ACCESS = 'af.access';
|
|
22
|
+
const REFRESH = 'af.refresh';
|
|
23
|
+
|
|
24
|
+
const tokenStore: TokenStore = {
|
|
25
|
+
getAccessToken: () => SecureStore.getItemAsync(ACCESS),
|
|
26
|
+
getRefreshToken: () => SecureStore.getItemAsync(REFRESH),
|
|
27
|
+
setTokens: async ({ accessToken, refreshToken }) => {
|
|
28
|
+
await SecureStore.setItemAsync(ACCESS, accessToken);
|
|
29
|
+
await SecureStore.setItemAsync(REFRESH, refreshToken);
|
|
30
|
+
},
|
|
31
|
+
clear: async () => {
|
|
32
|
+
await SecureStore.deleteItemAsync(ACCESS);
|
|
33
|
+
await SecureStore.deleteItemAsync(REFRESH);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const af = new ConsoleClient({
|
|
38
|
+
baseURL: process.env.EXPO_PUBLIC_AGENTFORGE_URL!,
|
|
39
|
+
tokenStore,
|
|
40
|
+
// Refresh was rejected or there is no refresh token: the store is already
|
|
41
|
+
// cleared, send the user to sign-in. Not fired by an explicit auth.logout().
|
|
42
|
+
onSignedOut: () => router.replace('/welcome'),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Sign in (stores the tokens through the TokenStore)
|
|
46
|
+
await af.auth.login({ email, password });
|
|
47
|
+
|
|
48
|
+
// Workspaces the user can access, with their role in each
|
|
49
|
+
const { items } = await af.tenants.listMine();
|
|
50
|
+
|
|
51
|
+
// Everything workspace-scoped hangs off forTenant()
|
|
52
|
+
const ws = af.forTenant(items[0].id);
|
|
53
|
+
const { items: teams } = await ws.teams.list();
|
|
54
|
+
const { groups } = await ws.tools.grouped({ q: 'mail' });
|
|
55
|
+
const { items: gallery } = await ws.templates.list({ kind: 'team' });
|
|
56
|
+
await ws.templates.install(gallery[0].id, { name: 'Mi tienda' });
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await ws.teams.publish(teamId);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e instanceof ApiError && e.code === 'team_not_publishable') {
|
|
62
|
+
console.log((e.details as { issues: unknown[] }).issues);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
On web, `MemoryTokenStore` (exported) or a `localStorage`-backed store works the same way.
|
|
68
|
+
|
|
69
|
+
## Options
|
|
70
|
+
|
|
71
|
+
| Option | Notes |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `baseURL` | Platform URL. A trailing slash is stripped. |
|
|
74
|
+
| `tokenStore` | `getAccessToken / getRefreshToken / setTokens / clear`, each sync or async. |
|
|
75
|
+
| `fetch` | Defaults to the global one. |
|
|
76
|
+
| `onSignedOut(reason)` | `'refresh_rejected' \| 'no_refresh_token'`. |
|
|
77
|
+
| `defaultHeaders` | Extra headers on every call. Any non-simple header makes browsers preflight. |
|
|
78
|
+
| `timeoutMs` | Default 30000; `0` disables. Timeouts reject as `ApiError` with `status: 0`, `code: 'timeout'`. |
|
|
79
|
+
| `crypto` | `OAuthCrypto` `{ getRandomValues(bytes), sha256(data) }` for OAuth PKCE where there is no global WebCrypto (React Native: wrap `expo-crypto`). |
|
|
80
|
+
| `maxRateLimitRetries` | GET replays on 429 honouring `Retry-After` (default 2). Writes are never replayed. |
|
|
81
|
+
|
|
82
|
+
A refresh that fails because of the network (offline, 5xx) does **not** sign the user out:
|
|
83
|
+
the original 401 is thrown and the session is kept.
|
|
84
|
+
|
|
85
|
+
## OAuth sign-in (Google / GitHub)
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const { url, codeVerifier } = await af.auth.oauthStart('google', { returnTo: 'poodleck-console://auth/callback' });
|
|
89
|
+
// open `url` in the browser; keep `codeVerifier` until the redirect comes back
|
|
90
|
+
const ret = parseOAuthReturn(redirectUrl); // { code } | { error } | null
|
|
91
|
+
if (ret && 'code' in ret) await af.auth.oauthComplete({ code: ret.code, codeVerifier });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`returnTo` must be in the server allow-list (400 `return_to_not_allowed`), and the
|
|
95
|
+
server requires the PKCE challenge (400 `pkce_required`). A bad / expired / reused
|
|
96
|
+
code answers 400 `invalid_grant`. On React Native pass the `crypto` option:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import * as Crypto from 'expo-crypto';
|
|
100
|
+
new ConsoleClient({ ..., crypto: {
|
|
101
|
+
getRandomValues: (b) => Crypto.getRandomValues(b),
|
|
102
|
+
sha256: async (d) => new Uint8Array(await Crypto.digest(Crypto.CryptoDigestAlgorithm.SHA256, d)),
|
|
103
|
+
} });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Errors
|
|
107
|
+
|
|
108
|
+
Every failure rejects with `ApiError`: `status` (0 = no response: `network_error` or
|
|
109
|
+
`timeout`), `code` (e.g. `captcha_failed`, `plan_exceeded`, `team_not_publishable`,
|
|
110
|
+
`invalid_config_schema`), `message`, and `details` (the parsed response body).
|
|
111
|
+
|
|
112
|
+
## Routes
|
|
113
|
+
|
|
114
|
+
Resource → methods → HTTP route (all JSON, bearer auth unless marked *public*).
|
|
115
|
+
`t` = `forTenant(tenantId)`, `:T` = `/admin/tenants/:tenantId`.
|
|
116
|
+
|
|
117
|
+
| Resource | Method | Route |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `auth` | `register` / `login` (public, store session) | `POST /auth/register` · `POST /auth/login` |
|
|
120
|
+
| | `refresh` | `POST /auth/refresh` |
|
|
121
|
+
| | `logout` | `POST /auth/logout` |
|
|
122
|
+
| | `me` | `GET /auth/me` |
|
|
123
|
+
| | `completeOnboarding` / `reopenOnboarding` | `POST /auth/me/onboarding/complete` · `.../reopen` |
|
|
124
|
+
| | `strategies` (public) | `GET /auth/strategies` |
|
|
125
|
+
| | `oauthStart` (no request) / `oauthComplete` (public, stores session) | URL `GET /auth/:provider?returnTo=&codeChallenge=` · `POST /auth/oauth/exchange` || `tenants` | `listMine` | `GET /me/tenants` |
|
|
126
|
+
| | `create` / `update` | `POST /admin/tenants` · `PATCH /admin/tenants/:id` |
|
|
127
|
+
| `t.members` | `list` / `add` | `GET` / `POST :T/members` |
|
|
128
|
+
| | `updateRole` / `remove` | `PATCH` / `DELETE :T/members/:membershipId` |
|
|
129
|
+
| | `transferOwnership` | `POST :T/members/transfer-ownership` |
|
|
130
|
+
| `t.invites` | `list` / `create` | `GET` / `POST :T/invites` |
|
|
131
|
+
| | `resend` / `revoke` | `POST :T/invites/:id/resend` · `DELETE :T/invites/:id` |
|
|
132
|
+
| `invites` (also on `t`) | `getByToken` / `accept` (public) | `GET /invite/:token` · `POST /invite/:token/accept` |
|
|
133
|
+
| `t.agents` | `list` `create` `get` `update` `delete` `parentTeam` | `:T/agents[/:id[/parent-team]]` (delete = deactivate) |
|
|
134
|
+
| `t.agents.chatTokens` | `list` `create` `update` `revoke` | `:T/agents/:id/chat-tokens[/:token]` |
|
|
135
|
+
| `t.teams` | `list` `create` `get` `update` `delete` | `:T/teams[/:id]` (`list({ excludeTemplateSources })`; teams carry `templateSourceOf`) |
|
|
136
|
+
| | `publication` / `listPublication` / `publish` | `GET :T/teams/:id/publication` · `GET :T/teams/publication` · `POST :T/teams/:id/publish` |
|
|
137
|
+
| `t.teams.humanMembers` | `list` `add` `update` `remove` | `:T/teams/:id/human-members[/:userId]` |
|
|
138
|
+
| `t.variables` | `list` `get` `upsert` `remove` | `GET /tenants/:tid/variables[?q=]` · `GET\|PUT\|DELETE /tenants/:tid/variables/:key` (config has `value`; secrets only `valueHint`) |
|
|
139
|
+
| `t.tools` | `grouped` | `GET /tools/grouped?tenantId=` |
|
|
140
|
+
| `t.connectors` | `directory` / `facets` (sends `X-Tenant-Id`) | `GET /connectors/oauth/directory[/facets]` |
|
|
141
|
+
| | `authorize` | `POST /connectors/oauth/:id/authorize?tenantId=&postAuthRedirect=` |
|
|
142
|
+
| | `saveApiKey` | `POST /connectors/oauth/:id/api-key?tenantId=` |
|
|
143
|
+
| `templates` | `listPublic` / `getPublic` (public) | `GET /public/templates[/:id]` |
|
|
144
|
+
| | `get` / `updateListing` / `listVersions` | `GET` / `PATCH /templates/:id` · `GET /templates/:id/versions` |
|
|
145
|
+
| | `signListingAsset` | `POST /templates/:id/listing-assets/sign` |
|
|
146
|
+
| | `versionHistory` / `diff` | `GET /templates/:id/version-history` · `GET /templates/:id/diff?from=&to=` |
|
|
147
|
+
| | `snapshotDraft` `submitVersion` `reviewVersion` `publishVersion` `discardVersion` `restoreVersion` | `POST /templates/:id/drafts` · `.../versions/:v/{submit,review,publish,restore}` · `DELETE .../versions/:v` |
|
|
148
|
+
| | `linkSource` `unlinkSource` `createSourceCopy` `setReviewPolicy` | `PUT/DELETE /templates/:id/source` · `POST .../source/copy` · `PATCH .../review-policy` |
|
|
149
|
+
| | `configFields` / `setConfigFields` | `GET` / `PUT /templates/:id/config-fields` |
|
|
150
|
+
| `t.templates` (extends `templates`) | `list` | `GET /templates?tenantId=` |
|
|
151
|
+
| | `install` / `installReadiness` | `POST /templates/:id/install` · `GET /templates/:id/install-readiness` |
|
|
152
|
+
| | `saveAgentAsTemplate` / `saveTeamAsTemplate` | `POST :T/{agents,teams}/:id/save-as-template` |
|
|
153
|
+
| | `publishAgentTemplateVersion` / `publishTeamTemplateVersion` | `POST :T/{agents,teams}/:id/publish-template-version` |
|
|
154
|
+
| | `templateSource` | `GET :T/teams/:id/template-source` |
|
|
155
|
+
| | `updatesAvailable` / `updateTeamFromTemplate` / `updateAgentFromTemplate` | `GET /me/template-updates-available` · `POST :T/{teams,agents}/:id/update-from-template` |
|
|
156
|
+
| `marketplace` | `categories` | `GET /marketplace/categories` |
|
|
157
|
+
| `t.apps` | `list` / `get` | `GET /apps[/:id]?tenantId=` |
|
|
158
|
+
| `t.playground` | `list` `get` `create` `finalize` `abort` | `/playgrounds/runs[/:id[/finalize\|abort]]` |
|
|
159
|
+
|
|
160
|
+
## Not covered (yet)
|
|
161
|
+
|
|
162
|
+
No `tenants.get` (the server has no `GET /admin/tenants/:id`: read it from `listMine`),
|
|
163
|
+
password
|
|
164
|
+
reset / email verification, Apps write operations, template pricing and earnings,
|
|
165
|
+
skills, trace/activity, the AI designer. They are tracked in the Console SDD.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
npm run build -w @agentforge-io/sdk-console
|
|
171
|
+
npm test -w @agentforge-io/sdk-console # tsx --test, fake fetch
|
|
172
|
+
npm run typecheck -w @agentforge-io/sdk-console
|
|
173
|
+
```
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { HttpCore, type ConsoleClientOptions } from './http';
|
|
2
|
+
import { AgentsResource } from './resources/agents';
|
|
3
|
+
import { AppsResource } from './resources/apps';
|
|
4
|
+
import { AuthResource } from './resources/auth';
|
|
5
|
+
import { ConnectorsResource } from './resources/connectors';
|
|
6
|
+
import { InvitesResource, MembersResource, PublicInvitesResource } from './resources/members';
|
|
7
|
+
import { MarketplaceResource } from './resources/marketplace';
|
|
8
|
+
import { PlaygroundResource } from './resources/playground';
|
|
9
|
+
import { TeamsResource } from './resources/teams';
|
|
10
|
+
import { TemplatesResource, TenantTemplatesResource } from './resources/templates';
|
|
11
|
+
import { TenantsResource } from './resources/tenants';
|
|
12
|
+
import { VariablesResource } from './resources/variables';
|
|
13
|
+
import { ToolsResource } from './resources/tools';
|
|
14
|
+
/** Resources of one workspace. Cheap to create: call `forTenant` per use. */
|
|
15
|
+
export declare class TenantScope {
|
|
16
|
+
readonly tenantId: string;
|
|
17
|
+
readonly members: MembersResource;
|
|
18
|
+
readonly invites: InvitesResource;
|
|
19
|
+
readonly agents: AgentsResource;
|
|
20
|
+
readonly teams: TeamsResource;
|
|
21
|
+
readonly tools: ToolsResource;
|
|
22
|
+
readonly connectors: ConnectorsResource;
|
|
23
|
+
readonly templates: TenantTemplatesResource;
|
|
24
|
+
readonly apps: AppsResource;
|
|
25
|
+
readonly playground: PlaygroundResource;
|
|
26
|
+
/** Business data of the workspace (`{{config:KEY}}` / `{{secret:KEY}}`). */
|
|
27
|
+
readonly variables: VariablesResource;
|
|
28
|
+
constructor(http: HttpCore, tenantId: string);
|
|
29
|
+
}
|
|
30
|
+
export declare class ConsoleClient {
|
|
31
|
+
readonly baseURL: string;
|
|
32
|
+
readonly auth: AuthResource;
|
|
33
|
+
readonly tenants: TenantsResource;
|
|
34
|
+
/** Public invite lookups / accept (no workspace needed). */
|
|
35
|
+
readonly invites: PublicInvitesResource;
|
|
36
|
+
/** Templates not bound to a workspace: gallery, detail, listing, versions. */
|
|
37
|
+
readonly templates: TemplatesResource;
|
|
38
|
+
readonly marketplace: MarketplaceResource;
|
|
39
|
+
private readonly http;
|
|
40
|
+
constructor(options: ConsoleClientOptions);
|
|
41
|
+
/** Bind the `admin/tenants/:tenantId/...` resources to one workspace. */
|
|
42
|
+
forTenant(tenantId: string): TenantScope;
|
|
43
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ConsoleClient: entry point. One instance per app; `forTenant(id)` binds the
|
|
3
|
+
// workspace-scoped resources.
|
|
4
|
+
//
|
|
5
|
+
// const af = new ConsoleClient({ baseURL, tokenStore, onSignedOut: () => router.replace('/sign-in') });
|
|
6
|
+
// await af.auth.login({ email, password });
|
|
7
|
+
// const { items } = await af.tenants.listMine();
|
|
8
|
+
// const agents = await af.forTenant(items[0].id).agents.list();
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ConsoleClient = exports.TenantScope = void 0;
|
|
11
|
+
const http_1 = require("./http");
|
|
12
|
+
const agents_1 = require("./resources/agents");
|
|
13
|
+
const apps_1 = require("./resources/apps");
|
|
14
|
+
const auth_1 = require("./resources/auth");
|
|
15
|
+
const connectors_1 = require("./resources/connectors");
|
|
16
|
+
const members_1 = require("./resources/members");
|
|
17
|
+
const marketplace_1 = require("./resources/marketplace");
|
|
18
|
+
const playground_1 = require("./resources/playground");
|
|
19
|
+
const teams_1 = require("./resources/teams");
|
|
20
|
+
const templates_1 = require("./resources/templates");
|
|
21
|
+
const tenants_1 = require("./resources/tenants");
|
|
22
|
+
const variables_1 = require("./resources/variables");
|
|
23
|
+
const tools_1 = require("./resources/tools");
|
|
24
|
+
/** Resources of one workspace. Cheap to create: call `forTenant` per use. */
|
|
25
|
+
class TenantScope {
|
|
26
|
+
constructor(http, tenantId) {
|
|
27
|
+
this.tenantId = tenantId;
|
|
28
|
+
if (!tenantId)
|
|
29
|
+
throw new Error('tenantId is required');
|
|
30
|
+
this.members = new members_1.MembersResource(http, tenantId);
|
|
31
|
+
this.invites = new members_1.InvitesResource(http, tenantId);
|
|
32
|
+
this.agents = new agents_1.AgentsResource(http, tenantId);
|
|
33
|
+
this.teams = new teams_1.TeamsResource(http, tenantId);
|
|
34
|
+
this.tools = new tools_1.ToolsResource(http, tenantId);
|
|
35
|
+
this.connectors = new connectors_1.ConnectorsResource(http, tenantId);
|
|
36
|
+
this.templates = new templates_1.TenantTemplatesResource(http, tenantId);
|
|
37
|
+
this.apps = new apps_1.AppsResource(http, tenantId);
|
|
38
|
+
this.playground = new playground_1.PlaygroundResource(http, tenantId);
|
|
39
|
+
this.variables = new variables_1.VariablesResource(http, tenantId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.TenantScope = TenantScope;
|
|
43
|
+
class ConsoleClient {
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.http = new http_1.HttpCore(options);
|
|
46
|
+
this.baseURL = this.http.baseURL;
|
|
47
|
+
this.auth = new auth_1.AuthResource(this.http);
|
|
48
|
+
this.tenants = new tenants_1.TenantsResource(this.http);
|
|
49
|
+
this.invites = new members_1.PublicInvitesResource(this.http);
|
|
50
|
+
this.templates = new templates_1.TemplatesResource(this.http);
|
|
51
|
+
this.marketplace = new marketplace_1.MarketplaceResource(this.http);
|
|
52
|
+
}
|
|
53
|
+
/** Bind the `admin/tenants/:tenantId/...` resources to one workspace. */
|
|
54
|
+
forTenant(tenantId) {
|
|
55
|
+
return new TenantScope(this.http, tenantId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.ConsoleClient = ConsoleClient;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ApiError extends Error {
|
|
2
|
+
/** HTTP status. `0` = the request never got a response (offline, DNS,
|
|
3
|
+
* timeout, aborted): see `code` (`network_error` | `timeout`). */
|
|
4
|
+
readonly status: number;
|
|
5
|
+
/** Machine-readable code from the body (`{ code }`, or a code-shaped
|
|
6
|
+
* `{ error }`), e.g. `captcha_failed`, `plan_exceeded`, `team_not_publishable`. */
|
|
7
|
+
readonly code?: string;
|
|
8
|
+
/** Parsed JSON body when the response had one, otherwise the raw text. */
|
|
9
|
+
readonly details?: unknown;
|
|
10
|
+
readonly cause?: unknown;
|
|
11
|
+
constructor(init: {
|
|
12
|
+
status: number;
|
|
13
|
+
message: string;
|
|
14
|
+
code?: string;
|
|
15
|
+
details?: unknown;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
});
|
|
18
|
+
get isNetworkError(): boolean;
|
|
19
|
+
get isUnauthorized(): boolean;
|
|
20
|
+
get isForbidden(): boolean;
|
|
21
|
+
get isNotFound(): boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function isApiError(err: unknown): err is ApiError;
|
|
24
|
+
/** Map a non-2xx response (body already read as text) to an ApiError. */
|
|
25
|
+
export declare function errorFromResponse(status: number, statusText: string, text: string): ApiError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Typed error every failed request rejects with. Branch on `status` / `code`;
|
|
3
|
+
// `details` carries the parsed response body (e.g. `{ issues }` of a 409
|
|
4
|
+
// `team_not_publishable`, `{ usage }` of a 402 `plan_exceeded`).
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ApiError = void 0;
|
|
7
|
+
exports.isApiError = isApiError;
|
|
8
|
+
exports.errorFromResponse = errorFromResponse;
|
|
9
|
+
class ApiError extends Error {
|
|
10
|
+
constructor(init) {
|
|
11
|
+
super(init.message);
|
|
12
|
+
this.name = 'ApiError';
|
|
13
|
+
this.status = init.status;
|
|
14
|
+
this.code = init.code;
|
|
15
|
+
this.details = init.details;
|
|
16
|
+
this.cause = init.cause;
|
|
17
|
+
}
|
|
18
|
+
get isNetworkError() {
|
|
19
|
+
return this.status === 0;
|
|
20
|
+
}
|
|
21
|
+
get isUnauthorized() {
|
|
22
|
+
return this.status === 401;
|
|
23
|
+
}
|
|
24
|
+
get isForbidden() {
|
|
25
|
+
return this.status === 403;
|
|
26
|
+
}
|
|
27
|
+
get isNotFound() {
|
|
28
|
+
return this.status === 404;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.ApiError = ApiError;
|
|
32
|
+
function isApiError(err) {
|
|
33
|
+
return err instanceof ApiError;
|
|
34
|
+
}
|
|
35
|
+
// `captcha_failed` / `INVALID_MAPPING` style, plus snake_case. Nest's status
|
|
36
|
+
// text ("Bad Request") matches neither, so it is never mistaken for a code.
|
|
37
|
+
const CODE_SHAPE = /^(?:[A-Z][A-Z0-9_]+|[a-z][a-z0-9]*(?:_[a-z0-9]+)+)$/;
|
|
38
|
+
// A `reason` is a deliberate machine value, so it is accepted looser than `error`
|
|
39
|
+
// (which may be Nest's status text): one token, no spaces (`forbidden`, `team_not_found`).
|
|
40
|
+
const REASON_SHAPE = /^[A-Za-z][A-Za-z0-9_.:-]{0,79}$/;
|
|
41
|
+
function messageOf(value) {
|
|
42
|
+
if (typeof value === 'string' && value)
|
|
43
|
+
return value;
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
const parts = value.filter((v) => typeof v === 'string');
|
|
46
|
+
return parts.length ? parts.join('; ') : undefined;
|
|
47
|
+
}
|
|
48
|
+
if (value && typeof value === 'object') {
|
|
49
|
+
return messageOf(value.message);
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Map a non-2xx response (body already read as text) to an ApiError. */
|
|
54
|
+
function errorFromResponse(status, statusText, text) {
|
|
55
|
+
let parsed = text || undefined;
|
|
56
|
+
if (text) {
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(text);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* not JSON: keep the raw text */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let code;
|
|
65
|
+
let message;
|
|
66
|
+
if (parsed && typeof parsed === 'object') {
|
|
67
|
+
const body = parsed;
|
|
68
|
+
if (typeof body.code === 'string')
|
|
69
|
+
code = body.code;
|
|
70
|
+
// Some routes send `{ reason: 'invalid_config_schema', message }`: `code` falls back to `reason`.
|
|
71
|
+
else if (typeof body.reason === 'string' && REASON_SHAPE.test(body.reason))
|
|
72
|
+
code = body.reason;
|
|
73
|
+
else if (typeof body.error === 'string' && CODE_SHAPE.test(body.error))
|
|
74
|
+
code = body.error;
|
|
75
|
+
else if (body.message && typeof body.message === 'object') {
|
|
76
|
+
const nested = body.message;
|
|
77
|
+
if (typeof nested.code === 'string')
|
|
78
|
+
code = nested.code;
|
|
79
|
+
else if (typeof nested.reason === 'string' && REASON_SHAPE.test(nested.reason))
|
|
80
|
+
code = nested.reason;
|
|
81
|
+
}
|
|
82
|
+
message = messageOf(body.message) ?? (typeof body.error === 'string' ? body.error : undefined);
|
|
83
|
+
}
|
|
84
|
+
else if (typeof parsed === 'string' && parsed.length < 300) {
|
|
85
|
+
message = parsed;
|
|
86
|
+
}
|
|
87
|
+
return new ApiError({
|
|
88
|
+
status,
|
|
89
|
+
message: message ?? (statusText ? `HTTP ${status} ${statusText}` : `HTTP ${status}`),
|
|
90
|
+
code,
|
|
91
|
+
details: parsed,
|
|
92
|
+
});
|
|
93
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { OAuthCrypto } from './oauth';
|
|
2
|
+
import type { AuthTokens, TokenStore } from './token-store';
|
|
3
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
4
|
+
/** Why the session ended without the user asking for it. */
|
|
5
|
+
export type SignedOutReason = 'refresh_rejected' | 'no_refresh_token';
|
|
6
|
+
export interface ConsoleClientOptions {
|
|
7
|
+
/** Platform base URL, e.g. `https://api.agentforge.example`. */
|
|
8
|
+
baseURL: string;
|
|
9
|
+
/** Where access + refresh tokens live. */
|
|
10
|
+
tokenStore: TokenStore;
|
|
11
|
+
/** Override fetch (RN polyfills, tests, custom transports). Defaults to the global one. */
|
|
12
|
+
fetch?: FetchLike;
|
|
13
|
+
/** Called after the session could not be salvaged (refresh rejected, or no
|
|
14
|
+
* refresh token): the token store is already cleared. Route to sign-in here.
|
|
15
|
+
* NOT called for an explicit `auth.logout()`. */
|
|
16
|
+
onSignedOut?: (reason: SignedOutReason) => void;
|
|
17
|
+
/** Headers added to every request (e.g. `ngrok-skip-browser-warning`). Note
|
|
18
|
+
* that any non-simple header makes browsers send a CORS preflight. */
|
|
19
|
+
defaultHeaders?: Record<string, string>;
|
|
20
|
+
/** Abort a request after this many ms. Default 30000; `0` disables. */
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
/** Replays of a GET answered 429, honouring `Retry-After`. Default 2. */
|
|
23
|
+
maxRateLimitRetries?: number;
|
|
24
|
+
/** Crypto adapter for OAuth PKCE (`auth.oauthStart`). Only needed where there
|
|
25
|
+
* is no global WebCrypto (`crypto.getRandomValues` + `crypto.subtle.digest`),
|
|
26
|
+
* i.e. React Native: pass an `expo-crypto` based one. */
|
|
27
|
+
crypto?: OAuthCrypto;
|
|
28
|
+
/** Sleep used between 429 replays. Injectable for tests. */
|
|
29
|
+
sleep?: (ms: number) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export type QueryValue = string | number | boolean | null | undefined | ReadonlyArray<string | number>;
|
|
32
|
+
export interface RequestOptions {
|
|
33
|
+
query?: Record<string, QueryValue>;
|
|
34
|
+
body?: unknown;
|
|
35
|
+
/** Extra headers for this call (e.g. `X-Tenant-Id`). */
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
/** `false` = public route: no Authorization header, never refreshes. */
|
|
38
|
+
auth?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Join a query object: skips null/undefined, arrays become `a,b`. */
|
|
41
|
+
export declare function buildQuery(query?: Record<string, QueryValue>): string;
|
|
42
|
+
export declare function seg(value: string): string;
|
|
43
|
+
export declare class HttpCore {
|
|
44
|
+
readonly baseURL: string;
|
|
45
|
+
readonly crypto?: OAuthCrypto;
|
|
46
|
+
private readonly store;
|
|
47
|
+
private readonly opts;
|
|
48
|
+
private refreshing;
|
|
49
|
+
private signingOut;
|
|
50
|
+
/** Bumped whenever the session is replaced or wiped; a refresh that started under an older epoch is stale. */
|
|
51
|
+
private epoch;
|
|
52
|
+
constructor(opts: ConsoleClientOptions);
|
|
53
|
+
/** Authenticated (or `auth: false`) JSON request. Resolves `undefined` on 204 / empty body. */
|
|
54
|
+
request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
55
|
+
/** Persist a session obtained from login / register / invite accept. */
|
|
56
|
+
setSession(tokens: AuthTokens): Promise<void>;
|
|
57
|
+
/** Invalidate any in-flight refresh without touching the store (call before
|
|
58
|
+
* an auth call that will replace or wipe the session). */
|
|
59
|
+
beginSessionChange(): void;
|
|
60
|
+
/** Wipe the session without firing `onSignedOut` (explicit logout). */
|
|
61
|
+
clearSession(): Promise<void>;
|
|
62
|
+
/** Force a refresh now (shares the in-flight one). Throws if it does not succeed. */
|
|
63
|
+
refreshNow(): Promise<AuthTokens>;
|
|
64
|
+
private send;
|
|
65
|
+
/** @returns true when the request should be replayed once. */
|
|
66
|
+
private recoverFrom401;
|
|
67
|
+
/** Idempotent: waiters of the same failed refresh all land here, but only
|
|
68
|
+
* one tears the session down and fires the callback. */
|
|
69
|
+
private signOut;
|
|
70
|
+
/** One /auth/refresh at a time: a burst of 401s awaits the same promise.
|
|
71
|
+
* The refresh token rotates on use, so a second concurrent call would be
|
|
72
|
+
* read as token reuse by the server and revoke the family. */
|
|
73
|
+
private refreshSingleFlight;
|
|
74
|
+
private doRefresh;
|
|
75
|
+
private rawFetch;
|
|
76
|
+
}
|