@agentforge-io/sdk-console 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
@@ -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` |
@@ -138,7 +160,7 @@ Resource → methods → HTTP route (all JSON, bearer auth unless marked *public
138
160
  ## Not covered (yet)
139
161
 
140
162
  No `tenants.get` (the server has no `GET /admin/tenants/:id`: read it from `listMine`),
141
- native Google/GitHub sign-in (the OAuth routes are browser redirects), password
163
+ password
142
164
  reset / email verification, Apps write operations, template pricing and earnings,
143
165
  skills, trace/activity, the AI designer. They are tracked in the Console SDD.
144
166
 
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; } });
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { HttpCore } from '../http';
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. */
@@ -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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/sdk-console",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {