@agentforge-io/sdk-console 0.1.0 → 0.3.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 +25 -2
- package/dist/http.d.ts +6 -0
- package/dist/http.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/oauth.d.ts +37 -0
- package/dist/oauth.js +97 -0
- package/dist/resources/auth.d.ts +22 -1
- package/dist/resources/auth.js +30 -0
- package/dist/resources/connectors.d.ts +7 -1
- package/dist/resources/connectors.js +12 -1
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,11 +76,33 @@ On web, `MemoryTokenStore` (exported) or a `localStorage`-backed store works the
|
|
|
76
76
|
| `onSignedOut(reason)` | `'refresh_rejected' \| 'no_refresh_token'`. |
|
|
77
77
|
| `defaultHeaders` | Extra headers on every call. Any non-simple header makes browsers preflight. |
|
|
78
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`). |
|
|
79
80
|
| `maxRateLimitRetries` | GET replays on 429 honouring `Retry-After` (default 2). Writes are never replayed. |
|
|
80
81
|
|
|
81
82
|
A refresh that fails because of the network (offline, 5xx) does **not** sign the user out:
|
|
82
83
|
the original 401 is thrown and the session is kept.
|
|
83
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
|
+
|
|
84
106
|
## Errors
|
|
85
107
|
|
|
86
108
|
Every failure rejects with `ApiError`: `status` (0 = no response: `network_error` or
|
|
@@ -100,7 +122,7 @@ Resource → methods → HTTP route (all JSON, bearer auth unless marked *public
|
|
|
100
122
|
| | `me` | `GET /auth/me` |
|
|
101
123
|
| | `completeOnboarding` / `reopenOnboarding` | `POST /auth/me/onboarding/complete` · `.../reopen` |
|
|
102
124
|
| | `strategies` (public) | `GET /auth/strategies` |
|
|
103
|
-
| `tenants` | `listMine` | `GET /me/tenants` |
|
|
125
|
+
| | `oauthStart` (no request) / `oauthComplete` (public, stores session) | URL `GET /auth/:provider?returnTo=&codeChallenge=` · `POST /auth/oauth/exchange` || `tenants` | `listMine` | `GET /me/tenants` |
|
|
104
126
|
| | `create` / `update` | `POST /admin/tenants` · `PATCH /admin/tenants/:id` |
|
|
105
127
|
| `t.members` | `list` / `add` | `GET` / `POST :T/members` |
|
|
106
128
|
| | `updateRole` / `remove` | `PATCH` / `DELETE :T/members/:membershipId` |
|
|
@@ -118,6 +140,7 @@ Resource → methods → HTTP route (all JSON, bearer auth unless marked *public
|
|
|
118
140
|
| `t.connectors` | `directory` / `facets` (sends `X-Tenant-Id`) | `GET /connectors/oauth/directory[/facets]` |
|
|
119
141
|
| | `authorize` | `POST /connectors/oauth/:id/authorize?tenantId=&postAuthRedirect=` |
|
|
120
142
|
| | `saveApiKey` | `POST /connectors/oauth/:id/api-key?tenantId=` |
|
|
143
|
+
| | `disconnect` | `DELETE /connectors/oauth/:id?tenantId=` |
|
|
121
144
|
| `templates` | `listPublic` / `getPublic` (public) | `GET /public/templates[/:id]` |
|
|
122
145
|
| | `get` / `updateListing` / `listVersions` | `GET` / `PATCH /templates/:id` · `GET /templates/:id/versions` |
|
|
123
146
|
| | `signListingAsset` | `POST /templates/:id/listing-assets/sign` |
|
|
@@ -138,7 +161,7 @@ Resource → methods → HTTP route (all JSON, bearer auth unless marked *public
|
|
|
138
161
|
## Not covered (yet)
|
|
139
162
|
|
|
140
163
|
No `tenants.get` (the server has no `GET /admin/tenants/:id`: read it from `listMine`),
|
|
141
|
-
|
|
164
|
+
password
|
|
142
165
|
reset / email verification, Apps write operations, template pricing and earnings,
|
|
143
166
|
skills, trace/activity, the AI designer. They are tracked in the Console SDD.
|
|
144
167
|
|
package/dist/http.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OAuthCrypto } from './oauth';
|
|
1
2
|
import type { AuthTokens, TokenStore } from './token-store';
|
|
2
3
|
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
3
4
|
/** Why the session ended without the user asking for it. */
|
|
@@ -20,6 +21,10 @@ export interface ConsoleClientOptions {
|
|
|
20
21
|
timeoutMs?: number;
|
|
21
22
|
/** Replays of a GET answered 429, honouring `Retry-After`. Default 2. */
|
|
22
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;
|
|
23
28
|
/** Sleep used between 429 replays. Injectable for tests. */
|
|
24
29
|
sleep?: (ms: number) => Promise<void>;
|
|
25
30
|
}
|
|
@@ -37,6 +42,7 @@ export declare function buildQuery(query?: Record<string, QueryValue>): string;
|
|
|
37
42
|
export declare function seg(value: string): string;
|
|
38
43
|
export declare class HttpCore {
|
|
39
44
|
readonly baseURL: string;
|
|
45
|
+
readonly crypto?: OAuthCrypto;
|
|
40
46
|
private readonly store;
|
|
41
47
|
private readonly opts;
|
|
42
48
|
private refreshing;
|
package/dist/http.js
CHANGED
|
@@ -47,6 +47,7 @@ class HttpCore {
|
|
|
47
47
|
this.opts = opts;
|
|
48
48
|
this.baseURL = opts.baseURL.replace(/\/+$/, '');
|
|
49
49
|
this.store = opts.tokenStore;
|
|
50
|
+
this.crypto = opts.crypto;
|
|
50
51
|
}
|
|
51
52
|
/** Authenticated (or `auth: false`) JSON request. Resolves `undefined` on 204 / empty body. */
|
|
52
53
|
async request(method, path, options = {}) {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { ConsoleClient, TenantScope } from './client';
|
|
|
2
2
|
export { ApiError, isApiError } from './errors';
|
|
3
3
|
export { MemoryTokenStore } from './token-store';
|
|
4
4
|
export type { AuthTokens, MaybePromise, TokenStore } from './token-store';
|
|
5
|
+
export { parseOAuthReturn } from './oauth';
|
|
6
|
+
export type { OAuthCrypto, OAuthReturn } from './oauth';
|
|
5
7
|
export type { ConsoleClientOptions, FetchLike, QueryValue, RequestOptions, SignedOutReason } from './http';
|
|
6
8
|
export * from './types';
|
|
7
9
|
export { AgentsResource, ChatTokensResource } from './resources/agents';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.VariablesResource = exports.ToolsResource = exports.TenantsResource = exports.TenantTemplatesResource = exports.TemplatesResource = exports.TeamsResource = exports.TeamHumanMembersResource = exports.PlaygroundResource = exports.MarketplaceResource = exports.PublicInvitesResource = exports.MembersResource = exports.InvitesResource = exports.ConnectorsResource = exports.AuthResource = exports.AppsResource = exports.ChatTokensResource = exports.AgentsResource = exports.MemoryTokenStore = exports.isApiError = exports.ApiError = exports.TenantScope = exports.ConsoleClient = void 0;
|
|
17
|
+
exports.VariablesResource = exports.ToolsResource = exports.TenantsResource = exports.TenantTemplatesResource = exports.TemplatesResource = exports.TeamsResource = exports.TeamHumanMembersResource = exports.PlaygroundResource = exports.MarketplaceResource = exports.PublicInvitesResource = exports.MembersResource = exports.InvitesResource = exports.ConnectorsResource = exports.AuthResource = exports.AppsResource = exports.ChatTokensResource = exports.AgentsResource = exports.parseOAuthReturn = exports.MemoryTokenStore = exports.isApiError = exports.ApiError = exports.TenantScope = exports.ConsoleClient = void 0;
|
|
18
18
|
var client_1 = require("./client");
|
|
19
19
|
Object.defineProperty(exports, "ConsoleClient", { enumerable: true, get: function () { return client_1.ConsoleClient; } });
|
|
20
20
|
Object.defineProperty(exports, "TenantScope", { enumerable: true, get: function () { return client_1.TenantScope; } });
|
|
@@ -23,6 +23,8 @@ Object.defineProperty(exports, "ApiError", { enumerable: true, get: function ()
|
|
|
23
23
|
Object.defineProperty(exports, "isApiError", { enumerable: true, get: function () { return errors_1.isApiError; } });
|
|
24
24
|
var token_store_1 = require("./token-store");
|
|
25
25
|
Object.defineProperty(exports, "MemoryTokenStore", { enumerable: true, get: function () { return token_store_1.MemoryTokenStore; } });
|
|
26
|
+
var oauth_1 = require("./oauth");
|
|
27
|
+
Object.defineProperty(exports, "parseOAuthReturn", { enumerable: true, get: function () { return oauth_1.parseOAuthReturn; } });
|
|
26
28
|
__exportStar(require("./types"), exports);
|
|
27
29
|
var agents_1 = require("./resources/agents");
|
|
28
30
|
Object.defineProperty(exports, "AgentsResource", { enumerable: true, get: function () { return agents_1.AgentsResource; } });
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal crypto surface needed for PKCE. Pass it as the `crypto` client option
|
|
3
|
+
* on runtimes without a global WebCrypto (React Native). With `expo-crypto`:
|
|
4
|
+
*
|
|
5
|
+
* crypto: {
|
|
6
|
+
* getRandomValues: (bytes) => Crypto.getRandomValues(bytes),
|
|
7
|
+
* sha256: async (data) => new Uint8Array(await Crypto.digest(Crypto.CryptoDigestAlgorithm.SHA256, data)),
|
|
8
|
+
* }
|
|
9
|
+
*/
|
|
10
|
+
export interface OAuthCrypto {
|
|
11
|
+
/** Fill `bytes` with cryptographically secure random values. May fill in
|
|
12
|
+
* place (return nothing) or return the filled array. */
|
|
13
|
+
getRandomValues(bytes: Uint8Array): Uint8Array | void;
|
|
14
|
+
/** SHA-256 digest (32 bytes) of `data`. */
|
|
15
|
+
sha256(data: Uint8Array): Promise<Uint8Array>;
|
|
16
|
+
}
|
|
17
|
+
export declare const CODE_VERIFIER_LENGTH = 64;
|
|
18
|
+
/** Resolve the adapter, falling back to the global WebCrypto. Throws if neither exists. */
|
|
19
|
+
export declare function resolveCrypto(adapter?: OAuthCrypto): OAuthCrypto;
|
|
20
|
+
/** RFC 7636 verifier: 64 chars of the unreserved set. */
|
|
21
|
+
export declare function generateCodeVerifier(c: OAuthCrypto): string;
|
|
22
|
+
export declare function base64UrlEncode(bytes: Uint8Array): string;
|
|
23
|
+
/** S256 challenge: base64url(SHA-256(ASCII(verifier))), no padding. */
|
|
24
|
+
export declare function codeChallengeS256(c: OAuthCrypto, verifier: string): Promise<string>;
|
|
25
|
+
/** Result of `parseOAuthReturn`. */
|
|
26
|
+
export type OAuthReturn = {
|
|
27
|
+
code: string;
|
|
28
|
+
} | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Read the fragment of the URL the server redirected to: `#code=...` on
|
|
33
|
+
* success, `#error=...` on failure. Works for `https://` and custom schemes
|
|
34
|
+
* (`poodleck-console://auth/callback#code=...`). Returns `null` when the URL
|
|
35
|
+
* carries neither. If both are present, `error` wins.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseOAuthReturn(url: string): OAuthReturn | null;
|
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// OAuth browser-redirect sign-in helpers (PKCE, RFC 7636). Pure and runtime
|
|
3
|
+
// agnostic: no Node `crypto`, no Buffer, no TextEncoder, no URLSearchParams, so
|
|
4
|
+
// it runs on Hermes / React Native, browsers and Node >= 19.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CODE_VERIFIER_LENGTH = void 0;
|
|
7
|
+
exports.resolveCrypto = resolveCrypto;
|
|
8
|
+
exports.generateCodeVerifier = generateCodeVerifier;
|
|
9
|
+
exports.base64UrlEncode = base64UrlEncode;
|
|
10
|
+
exports.codeChallengeS256 = codeChallengeS256;
|
|
11
|
+
exports.parseOAuthReturn = parseOAuthReturn;
|
|
12
|
+
const UNRESERVED = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
13
|
+
const B64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
14
|
+
/** Largest multiple of 66 below 256: bytes at or above it are rejected (no modulo bias). */
|
|
15
|
+
const REJECT_FROM = 198;
|
|
16
|
+
exports.CODE_VERIFIER_LENGTH = 64;
|
|
17
|
+
/** Resolve the adapter, falling back to the global WebCrypto. Throws if neither exists. */
|
|
18
|
+
function resolveCrypto(adapter) {
|
|
19
|
+
if (adapter)
|
|
20
|
+
return adapter;
|
|
21
|
+
const g = globalThis.crypto;
|
|
22
|
+
if (g && typeof g.getRandomValues === 'function' && g.subtle && typeof g.subtle.digest === 'function') {
|
|
23
|
+
return {
|
|
24
|
+
getRandomValues: (bytes) => g.getRandomValues(bytes),
|
|
25
|
+
sha256: async (data) => new Uint8Array(await g.subtle.digest('SHA-256', data)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
throw new Error('No crypto available for OAuth PKCE: pass a `crypto` adapter ({ getRandomValues, sha256 }) in the client options');
|
|
29
|
+
}
|
|
30
|
+
function randomBytes(c, length) {
|
|
31
|
+
const bytes = new Uint8Array(length);
|
|
32
|
+
return c.getRandomValues(bytes) ?? bytes;
|
|
33
|
+
}
|
|
34
|
+
/** RFC 7636 verifier: 64 chars of the unreserved set. */
|
|
35
|
+
function generateCodeVerifier(c) {
|
|
36
|
+
let out = '';
|
|
37
|
+
while (out.length < exports.CODE_VERIFIER_LENGTH) {
|
|
38
|
+
const bytes = randomBytes(c, exports.CODE_VERIFIER_LENGTH * 2);
|
|
39
|
+
for (let i = 0; i < bytes.length && out.length < exports.CODE_VERIFIER_LENGTH; i++) {
|
|
40
|
+
if (bytes[i] < REJECT_FROM)
|
|
41
|
+
out += UNRESERVED[bytes[i] % UNRESERVED.length];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function base64UrlEncode(bytes) {
|
|
47
|
+
let out = '';
|
|
48
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
49
|
+
const n = (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);
|
|
50
|
+
out += B64URL[(n >> 18) & 63] + B64URL[(n >> 12) & 63];
|
|
51
|
+
if (i + 1 < bytes.length)
|
|
52
|
+
out += B64URL[(n >> 6) & 63];
|
|
53
|
+
if (i + 2 < bytes.length)
|
|
54
|
+
out += B64URL[n & 63];
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** S256 challenge: base64url(SHA-256(ASCII(verifier))), no padding. */
|
|
59
|
+
async function codeChallengeS256(c, verifier) {
|
|
60
|
+
const data = new Uint8Array(verifier.length);
|
|
61
|
+
for (let i = 0; i < verifier.length; i++)
|
|
62
|
+
data[i] = verifier.charCodeAt(i) & 0xff;
|
|
63
|
+
const digest = await c.sha256(data);
|
|
64
|
+
return base64UrlEncode(digest);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read the fragment of the URL the server redirected to: `#code=...` on
|
|
68
|
+
* success, `#error=...` on failure. Works for `https://` and custom schemes
|
|
69
|
+
* (`poodleck-console://auth/callback#code=...`). Returns `null` when the URL
|
|
70
|
+
* carries neither. If both are present, `error` wins.
|
|
71
|
+
*/
|
|
72
|
+
function parseOAuthReturn(url) {
|
|
73
|
+
if (typeof url !== 'string')
|
|
74
|
+
return null;
|
|
75
|
+
const hash = url.indexOf('#');
|
|
76
|
+
if (hash < 0)
|
|
77
|
+
return null;
|
|
78
|
+
const params = {};
|
|
79
|
+
for (const pair of url.slice(hash + 1).split('&')) {
|
|
80
|
+
if (!pair)
|
|
81
|
+
continue;
|
|
82
|
+
const eq = pair.indexOf('=');
|
|
83
|
+
const rawKey = eq < 0 ? pair : pair.slice(0, eq);
|
|
84
|
+
const rawVal = eq < 0 ? '' : pair.slice(eq + 1);
|
|
85
|
+
try {
|
|
86
|
+
params[decodeURIComponent(rawKey.replace(/\+/g, ' '))] = decodeURIComponent(rawVal.replace(/\+/g, ' '));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// malformed escape: skip this pair
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (params.error)
|
|
93
|
+
return { error: params.error };
|
|
94
|
+
if (params.code)
|
|
95
|
+
return { code: params.code };
|
|
96
|
+
return null;
|
|
97
|
+
}
|
package/dist/resources/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type HttpCore } from '../http';
|
|
2
2
|
import type { AuthResult, AuthStrategies, AuthUser, LoginInput, RegisterInput } from '../types';
|
|
3
3
|
export declare class AuthResource {
|
|
4
4
|
private readonly http;
|
|
@@ -8,6 +8,27 @@ export declare class AuthResource {
|
|
|
8
8
|
/** `POST /auth/login`. Stores the session. 403 `captcha_failed` when the
|
|
9
9
|
* server enforces Turnstile and no `captchaToken` was sent. */
|
|
10
10
|
login(input: LoginInput): Promise<AuthResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Start a browser OAuth sign-in (PKCE). Builds `GET /auth/:provider?returnTo=&codeChallenge=`
|
|
13
|
+
* (no network call): open `url` in the browser, keep `codeVerifier` (in
|
|
14
|
+
* memory / sessionStorage), and when the browser lands on `returnTo#code=...`
|
|
15
|
+
* call `oauthComplete`. `returnTo` must be in the server allow-list, else
|
|
16
|
+
* opening `url` answers 400 `return_to_not_allowed`. Needs a global WebCrypto
|
|
17
|
+
* or the `crypto` client option; throws otherwise.
|
|
18
|
+
*/
|
|
19
|
+
oauthStart(provider: 'google' | 'github' | (string & {}), input: {
|
|
20
|
+
returnTo: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
url: string;
|
|
23
|
+
codeVerifier: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** `POST /auth/oauth/exchange` (public): trade the one-time `code` from the
|
|
26
|
+
* redirect + the `codeVerifier` from `oauthStart` for a session. Stores it
|
|
27
|
+
* like `login`. 400 `invalid_grant` when expired, reused or mismatched. */
|
|
28
|
+
oauthComplete(input: {
|
|
29
|
+
code: string;
|
|
30
|
+
codeVerifier: string;
|
|
31
|
+
}): Promise<AuthResult>;
|
|
11
32
|
/** `POST /auth/refresh` with the stored refresh token (single-flight with
|
|
12
33
|
* the automatic refresh). Rotates and stores the new pair. Not needed in
|
|
13
34
|
* normal use: a 401 refreshes by itself. */
|
package/dist/resources/auth.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// touch them. Routes: apps/server identity AuthController / OAuthController.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.AuthResource = void 0;
|
|
6
|
+
const http_1 = require("../http");
|
|
7
|
+
const oauth_1 = require("../oauth");
|
|
6
8
|
class AuthResource {
|
|
7
9
|
constructor(http) {
|
|
8
10
|
this.http = http;
|
|
@@ -20,6 +22,34 @@ class AuthResource {
|
|
|
20
22
|
await this.http.setSession(res);
|
|
21
23
|
return res;
|
|
22
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Start a browser OAuth sign-in (PKCE). Builds `GET /auth/:provider?returnTo=&codeChallenge=`
|
|
27
|
+
* (no network call): open `url` in the browser, keep `codeVerifier` (in
|
|
28
|
+
* memory / sessionStorage), and when the browser lands on `returnTo#code=...`
|
|
29
|
+
* call `oauthComplete`. `returnTo` must be in the server allow-list, else
|
|
30
|
+
* opening `url` answers 400 `return_to_not_allowed`. Needs a global WebCrypto
|
|
31
|
+
* or the `crypto` client option; throws otherwise.
|
|
32
|
+
*/
|
|
33
|
+
async oauthStart(provider, input) {
|
|
34
|
+
if (!input?.returnTo)
|
|
35
|
+
throw new Error('returnTo is required');
|
|
36
|
+
const crypto = (0, oauth_1.resolveCrypto)(this.http.crypto);
|
|
37
|
+
const codeVerifier = (0, oauth_1.generateCodeVerifier)(crypto);
|
|
38
|
+
const codeChallenge = await (0, oauth_1.codeChallengeS256)(crypto, codeVerifier);
|
|
39
|
+
const query = (0, http_1.buildQuery)({ returnTo: input.returnTo, codeChallenge });
|
|
40
|
+
return { url: `${this.http.baseURL}/auth/${(0, http_1.seg)(provider)}${query}`, codeVerifier };
|
|
41
|
+
}
|
|
42
|
+
/** `POST /auth/oauth/exchange` (public): trade the one-time `code` from the
|
|
43
|
+
* redirect + the `codeVerifier` from `oauthStart` for a session. Stores it
|
|
44
|
+
* like `login`. 400 `invalid_grant` when expired, reused or mismatched. */
|
|
45
|
+
async oauthComplete(input) {
|
|
46
|
+
const res = await this.http.request('POST', '/auth/oauth/exchange', {
|
|
47
|
+
body: { code: input.code, codeVerifier: input.codeVerifier },
|
|
48
|
+
auth: false,
|
|
49
|
+
});
|
|
50
|
+
await this.http.setSession(res);
|
|
51
|
+
return res;
|
|
52
|
+
}
|
|
23
53
|
/** `POST /auth/refresh` with the stored refresh token (single-flight with
|
|
24
54
|
* the automatic refresh). Rotates and stores the new pair. Not needed in
|
|
25
55
|
* normal use: a 401 refreshes by itself. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpCore } from '../http';
|
|
2
|
-
import type { AuthorizeConnectorInput, ConnectorFacets, ConnectorStatus, ListConnectorsInput } from '../types';
|
|
2
|
+
import type { AuthorizeConnectorInput, ConnectorFacets, ConnectorStatus, DisconnectConnectorResult, ListConnectorsInput } from '../types';
|
|
3
3
|
export declare class ConnectorsResource {
|
|
4
4
|
private readonly http;
|
|
5
5
|
private readonly tenantId;
|
|
@@ -22,4 +22,10 @@ export declare class ConnectorsResource {
|
|
|
22
22
|
saveApiKey(connectorId: string, apiKey: string): Promise<{
|
|
23
23
|
connectorId: string;
|
|
24
24
|
}>;
|
|
25
|
+
/** `DELETE /connectors/oauth/:id?tenantId=`: removes this workspace's
|
|
26
|
+
* connection (owner/admin; best-effort revoke at the provider). Idempotent:
|
|
27
|
+
* `disconnected: false` when there was nothing to remove. A
|
|
28
|
+
* `postAuthRedirect` outside the platform allow-list makes `authorize`
|
|
29
|
+
* fail with `ApiError` code `post_auth_redirect_not_allowed`. */
|
|
30
|
+
disconnect(connectorId: string): Promise<DisconnectConnectorResult>;
|
|
25
31
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// connectors for one workspace (`forTenant(id).connectors`): the directory
|
|
3
|
-
// with per-workspace `connected` flags,
|
|
3
|
+
// with per-workspace `connected` flags, the OAuth / API-key connect flow and
|
|
4
|
+
// disconnect.
|
|
4
5
|
//
|
|
5
6
|
// The directory resolves the workspace from the `X-Tenant-Id` header (the JWT
|
|
6
7
|
// carries no tenant), so these calls send it. A browser therefore needs CORS
|
|
@@ -43,5 +44,15 @@ class ConnectorsResource {
|
|
|
43
44
|
body: { apiKey },
|
|
44
45
|
});
|
|
45
46
|
}
|
|
47
|
+
/** `DELETE /connectors/oauth/:id?tenantId=`: removes this workspace's
|
|
48
|
+
* connection (owner/admin; best-effort revoke at the provider). Idempotent:
|
|
49
|
+
* `disconnected: false` when there was nothing to remove. A
|
|
50
|
+
* `postAuthRedirect` outside the platform allow-list makes `authorize`
|
|
51
|
+
* fail with `ApiError` code `post_auth_redirect_not_allowed`. */
|
|
52
|
+
async disconnect(connectorId) {
|
|
53
|
+
return this.http.request('DELETE', `/connectors/oauth/${(0, http_1.seg)(connectorId)}`, {
|
|
54
|
+
query: { tenantId: this.tenantId },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
46
57
|
}
|
|
47
58
|
exports.ConnectorsResource = ConnectorsResource;
|
package/dist/types.d.ts
CHANGED
|
@@ -1007,3 +1007,13 @@ export interface PublisherErrorBody {
|
|
|
1007
1007
|
message: string;
|
|
1008
1008
|
issues?: TeamPublicationIssue[];
|
|
1009
1009
|
}
|
|
1010
|
+
/** `DELETE /connectors/oauth/:id` result. */
|
|
1011
|
+
export interface DisconnectConnectorResult {
|
|
1012
|
+
ok: true;
|
|
1013
|
+
connectorId: string;
|
|
1014
|
+
tenantId: string;
|
|
1015
|
+
/** False when the workspace had no connection (idempotent). */
|
|
1016
|
+
disconnected: boolean;
|
|
1017
|
+
/** True when the provider confirmed the token revoke (best effort). */
|
|
1018
|
+
revokedAtProvider: boolean;
|
|
1019
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/sdk-console",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "User-session SDK for admin clients of the AgentForge platform (Expo / React Native, web, Node). Dashboard JWT auth with refresh, tenant scoping, typed resources.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|