@arinova-ai/spaces-sdk 0.1.1 → 0.1.3

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
@@ -1,109 +1,91 @@
1
- # @arinova-ai/spaces-sdk
1
+ # @arinova/spaces-sdk
2
2
 
3
- Official SDK for integrating external games with the Arinova platform.
3
+ OAuth PKCE SDK for Arinova Spaces authenticate users without exposing secrets.
4
4
 
5
- ## Install
5
+ ## Installation
6
6
 
7
7
  ```bash
8
- npm install @arinova-ai/spaces-sdk
8
+ npm install @arinova/spaces-sdk
9
9
  ```
10
10
 
11
11
  ## Quick Start
12
12
 
13
- ### 1. Initialize
13
+ ```js
14
+ import { Arinova } from "@arinova/spaces-sdk";
14
15
 
15
- ```typescript
16
- import { Arinova } from "@arinova-ai/spaces-sdk";
16
+ const arinova = new Arinova({ appId: "your-client-id" });
17
17
 
18
- Arinova.init({
19
- appId: "your-client-id",
20
- baseUrl: "https://api.arinova.ai", // optional, defaults to production
21
- });
18
+ // Trigger login (opens popup)
19
+ const token = await arinova.login();
20
+ console.log(token.user.name); // "Alice"
21
+ console.log(token.access_token); // Bearer token for API calls
22
22
  ```
23
23
 
24
- ### 2a. Connect (Recommended)
24
+ ## Setup
25
25
 
26
- The easiest way to authenticate. Works automatically in both contexts:
26
+ 1. Register your app: `arinova-cli app create --name "My App" --redirect-uri "https://myapp.com"`
27
+ 2. Copy the `Client ID` from the output
28
+ 3. No `client_secret` needed — all apps use PKCE
27
29
 
28
- - **Embedded in Arinova Chat iframe**: receives auth via `postMessage` from the parent window (no redirect needed).
29
- - **Standalone (outside iframe)**: falls back to the OAuth login flow.
30
+ ## API
30
31
 
31
- ```typescript
32
- const { user, accessToken, agents } = await Arinova.connect({ timeout: 5000 });
33
- // user: { id, name, email, image }
34
- // accessToken: session token for API calls
35
- // agents: user's connected agents (may be empty)
36
- ```
32
+ ### `new Arinova(config)`
37
33
 
38
- ### 2b. Login (Manual OAuth Flow)
34
+ | Option | Type | Default | Description |
35
+ |--------|------|---------|-------------|
36
+ | `appId` | `string` | *required* | Your OAuth app client_id |
37
+ | `endpoint` | `string` | `https://chat.arinova.ai` | Arinova server URL |
38
+ | `redirectUri` | `string` | `{origin}/callback` | OAuth callback URL |
39
+ | `scope` | `string` | `"profile"` | OAuth scope |
39
40
 
40
- ```typescript
41
- // Redirect to Arinova login
42
- Arinova.login({ scope: ["profile", "agents"] });
41
+ ### `arinova.login(): Promise<TokenResponse>`
43
42
 
44
- // Handle callback (on your redirect page)
45
- const { user, accessToken } = await Arinova.handleCallback({
46
- code: urlParams.get("code"),
47
- clientId: "your-client-id",
48
- clientSecret: "your-client-secret",
49
- redirectUri: window.location.origin + "/callback",
50
- });
43
+ Opens a popup for user authorization (PKCE flow). Returns:
44
+
45
+ ```ts
46
+ {
47
+ access_token: string;
48
+ token_type: "Bearer";
49
+ expires_in: 604800; // 7 days
50
+ scope: "profile";
51
+ user: { id, name, email, image };
52
+ }
51
53
  ```
52
54
 
53
- ### 3. Use Agent API
55
+ ### `arinova.handleCallback(): Promise<TokenResponse>`
54
56
 
55
- ```typescript
56
- // Get user's agents
57
- const agents = await Arinova.user.agents(accessToken);
57
+ Call on your redirect_uri page to complete the flow (for redirect mode instead of popup).
58
58
 
59
- // Chat with agent (sync)
60
- const { response } = await Arinova.agent.chat({
61
- agentId: agents[0].id,
62
- prompt: "Your board state...",
63
- accessToken,
64
- });
59
+ ## PKCE Flow
65
60
 
66
- // Chat with agent (streaming)
67
- const result = await Arinova.agent.chatStream({
68
- agentId: agents[0].id,
69
- prompt: "Your move?",
70
- accessToken,
71
- onChunk: (chunk) => console.log("Streaming:", chunk),
72
- });
73
- ```
61
+ 1. SDK generates `code_verifier` (random) and `code_challenge = BASE64URL(SHA256(code_verifier))`
62
+ 2. User is redirected to Arinova with `code_challenge`
63
+ 3. After authorization, Arinova redirects back with `code`
64
+ 4. SDK exchanges `code` + `code_verifier` for `access_token` (no secret needed)
65
+
66
+ ## redirect_uri Rules
67
+
68
+ - Origin match: scheme + host + port must match your registered URI
69
+ - Path can differ (SDK uses `window.location.origin + /callback` by default)
70
+ - Must use HTTPS in production
71
+ - `http://localhost` is allowed for development
72
+
73
+ ## Example: Redirect Mode
74
74
 
75
- ### 4. Economy (Server-to-Server)
75
+ If popups are blocked, use redirect mode:
76
76
 
77
- ```typescript
78
- // Charge coins
79
- const { newBalance } = await Arinova.economy.charge({
80
- userId: "user-id",
81
- amount: 10,
82
- description: "Game entry fee",
83
- clientId: "your-client-id",
84
- clientSecret: "your-client-secret",
77
+ ```js
78
+ // On login page:
79
+ const arinova = new Arinova({
80
+ appId: "your-client-id",
81
+ redirectUri: "https://myapp.com/auth/callback",
85
82
  });
83
+ arinova.login(); // Will redirect if popup is blocked
86
84
 
87
- // Award coins
88
- const { newBalance, platformFee } = await Arinova.economy.award({
89
- userId: "user-id",
90
- amount: 20,
91
- description: "Game prize",
92
- clientId: "your-client-id",
93
- clientSecret: "your-client-secret",
85
+ // On callback page (/auth/callback):
86
+ const arinova = new Arinova({
87
+ appId: "your-client-id",
88
+ redirectUri: "https://myapp.com/auth/callback",
94
89
  });
90
+ const token = await arinova.handleCallback();
95
91
  ```
96
-
97
- ## API Reference
98
-
99
- ### `Arinova.init(config)`
100
- ### `Arinova.connect(options?)`
101
- ### `Arinova.login(options?)`
102
- ### `Arinova.handleCallback(params)`
103
- ### `Arinova.user.profile(accessToken)`
104
- ### `Arinova.user.agents(accessToken)`
105
- ### `Arinova.agent.chat(options)`
106
- ### `Arinova.agent.chatStream(options)`
107
- ### `Arinova.economy.charge(options)`
108
- ### `Arinova.economy.award(options)`
109
- ### `Arinova.economy.balance(accessToken)`
package/dist/index.d.ts CHANGED
@@ -1,80 +1,85 @@
1
- import type { ArinovaConfig, LoginOptions, LoginResult, ConnectOptions, ConnectResult, ArinovaUser, AgentInfo, AgentChatOptions, AgentChatResponse, AgentChatStreamOptions, AgentChatStreamResponse, ChargeOptions, ChargeResponse, AwardOptions, AwardResponse, BalanceResponse } from "./types.js";
2
- export type * from "./types.js";
3
- export declare const Arinova: {
4
- /**
5
- * Initialize the SDK with your app configuration.
6
- */
7
- init(config: ArinovaConfig & {
8
- clientId?: string;
9
- clientSecret?: string;
10
- }): void;
1
+ /**
2
+ * Arinova Spaces SDK
3
+ *
4
+ * Public client OAuth with PKCE — no client_secret needed.
5
+ *
6
+ * Usage:
7
+ * const arinova = new Arinova({ appId: "my-app-abc123" });
8
+ * const token = await arinova.login();
9
+ * // token.access_token is ready to use
10
+ */
11
+ export interface ArinovaConfig {
12
+ /** Your OAuth app's client_id (from Developer Console) */
13
+ appId: string;
14
+ /** Arinova server URL (default: https://chat.arinova.ai) */
15
+ endpoint?: string;
16
+ /** OAuth redirect URI (default: current page origin + /callback) */
17
+ redirectUri?: string;
18
+ /** OAuth scope (default: "profile") */
19
+ scope?: string;
20
+ }
21
+ export interface TokenResponse {
22
+ access_token: string;
23
+ token_type: string;
24
+ expires_in: number;
25
+ scope: string;
26
+ user: {
27
+ id: string;
28
+ name: string;
29
+ email: string;
30
+ image: string | null;
31
+ };
32
+ }
33
+ export interface BalanceResponse {
34
+ balance: number;
35
+ }
36
+ export interface PurchaseResponse {
37
+ transactionId: string;
38
+ newBalance: number;
39
+ }
40
+ export interface TransactionRecord {
41
+ id: string;
42
+ type: string;
43
+ amount: number;
44
+ description: string | null;
45
+ createdAt: string;
46
+ }
47
+ export interface TransactionsResponse {
48
+ transactions: TransactionRecord[];
49
+ total: number;
50
+ limit: number;
51
+ offset: number;
52
+ }
53
+ export declare class Arinova {
54
+ private appId;
55
+ private endpoint;
56
+ private redirectUri;
57
+ private scope;
58
+ /** The access token from the most recent login/handleCallback. */
59
+ accessToken: string | null;
60
+ constructor(config: ArinovaConfig);
11
61
  /**
12
- * Connect to Arinova works seamlessly in both iframe and standalone contexts.
13
- *
14
- * - **Inside an iframe** (embedded in Arinova Chat): receives auth via postMessage from the parent window.
15
- * - **Outside an iframe** (standalone): falls back to the OAuth login() flow.
16
- *
17
- * @param options.timeout - How long to wait for postMessage in iframe mode (default: 5000ms)
18
- * @returns Promise resolving with user, accessToken, and agents
62
+ * Start the OAuth PKCE login flow.
63
+ * Opens a popup window for authorization.
64
+ * Returns the token response on success.
19
65
  */
20
- connect(options?: ConnectOptions): Promise<ConnectResult>;
66
+ login(): Promise<TokenResponse>;
21
67
  /**
22
- * Redirect to Arinova OAuth login page.
23
- * Call this from your game's frontend.
68
+ * Handle the OAuth callback (call this on your redirect_uri page).
69
+ * Reads code and state from URL, exchanges for token.
24
70
  */
25
- login(options?: LoginOptions): void;
71
+ handleCallback(): Promise<TokenResponse>;
72
+ private getToken;
73
+ private apiFetch;
74
+ /** Get the current user's coin balance. */
75
+ balance(): Promise<BalanceResponse>;
26
76
  /**
27
- * Handle the OAuth callback. Call this on your redirect page.
28
- * Exchanges the authorization code for an access token.
77
+ * Purchase / charge coins from the user's balance.
78
+ * Requires the "economy" scope.
29
79
  */
30
- handleCallback(params: {
31
- code: string;
32
- clientId: string;
33
- clientSecret: string;
34
- redirectUri: string;
35
- }): Promise<LoginResult>;
36
- user: {
37
- /**
38
- * Get the authenticated user's profile.
39
- */
40
- profile(accessToken: string): Promise<ArinovaUser>;
41
- /**
42
- * Get the authenticated user's agents.
43
- * Requires "agents" scope.
44
- */
45
- agents(accessToken: string): Promise<AgentInfo[]>;
46
- };
47
- agent: {
48
- /**
49
- * Send a prompt to a user's agent and get a complete response.
50
- */
51
- chat(options: AgentChatOptions): Promise<AgentChatResponse>;
52
- /**
53
- * Send a prompt to a user's agent and receive a streaming response via SSE.
54
- */
55
- chatStream(options: AgentChatStreamOptions): Promise<AgentChatStreamResponse>;
56
- };
57
- economy: {
58
- /**
59
- * Charge coins from a user's balance (server-to-server).
60
- * Requires clientId and clientSecret.
61
- */
62
- charge(options: ChargeOptions & {
63
- clientId: string;
64
- clientSecret: string;
65
- }): Promise<ChargeResponse>;
66
- /**
67
- * Award coins to a user (server-to-server).
68
- * Requires clientId and clientSecret.
69
- */
70
- award(options: AwardOptions & {
71
- clientId: string;
72
- clientSecret: string;
73
- }): Promise<AwardResponse>;
74
- /**
75
- * Get a user's coin balance (uses OAuth access token).
76
- */
77
- balance(accessToken: string): Promise<BalanceResponse>;
78
- };
79
- };
80
- //# sourceMappingURL=index.d.ts.map
80
+ purchase(productId: string, amount: number, description?: string): Promise<PurchaseResponse>;
81
+ /** Get the user's transaction history. */
82
+ transactions(limit?: number, offset?: number): Promise<TransactionsResponse>;
83
+ private exchangeCode;
84
+ }
85
+ export default Arinova;
package/dist/index.js CHANGED
@@ -1,284 +1,209 @@
1
- let _config = null;
2
- let _oauthClientInfo = null;
3
- function getBaseUrl() {
4
- if (!_config)
5
- throw new Error("Arinova SDK not initialized. Call Arinova.init() first.");
6
- return _config.baseUrl || "https://api.arinova.ai";
7
- }
8
- function getConfig() {
9
- if (!_config)
10
- throw new Error("Arinova SDK not initialized. Call Arinova.init() first.");
11
- return _config;
12
- }
13
- export const Arinova = {
14
- /**
15
- * Initialize the SDK with your app configuration.
16
- */
17
- init(config) {
18
- _config = config;
19
- if (config.clientId && config.clientSecret) {
20
- _oauthClientInfo = { clientId: config.clientId, clientSecret: config.clientSecret };
21
- }
22
- },
1
+ /**
2
+ * Arinova Spaces SDK
3
+ *
4
+ * Public client OAuth with PKCE — no client_secret needed.
5
+ *
6
+ * Usage:
7
+ * const arinova = new Arinova({ appId: "my-app-abc123" });
8
+ * const token = await arinova.login();
9
+ * // token.access_token is ready to use
10
+ */
11
+ export class Arinova {
12
+ constructor(config) {
13
+ /** The access token from the most recent login/handleCallback. */
14
+ this.accessToken = null;
15
+ this.appId = config.appId;
16
+ this.endpoint = (config.endpoint ?? "https://chat.arinova.ai").replace(/\/+$/, "");
17
+ this.redirectUri =
18
+ config.redirectUri ?? `${window.location.origin}/callback`;
19
+ this.scope = config.scope ?? "profile";
20
+ }
23
21
  /**
24
- * Connect to Arinova works seamlessly in both iframe and standalone contexts.
25
- *
26
- * - **Inside an iframe** (embedded in Arinova Chat): receives auth via postMessage from the parent window.
27
- * - **Outside an iframe** (standalone): falls back to the OAuth login() flow.
28
- *
29
- * @param options.timeout - How long to wait for postMessage in iframe mode (default: 5000ms)
30
- * @returns Promise resolving with user, accessToken, and agents
22
+ * Start the OAuth PKCE login flow.
23
+ * Opens a popup window for authorization.
24
+ * Returns the token response on success.
31
25
  */
32
- async connect(options) {
33
- const timeout = options?.timeout ?? 5000;
34
- const inIframe = typeof window !== "undefined" && window.self !== window.top;
35
- if (!inIframe) {
36
- // Not in iframe — fall back to OAuth login flow
37
- this.login();
38
- // login() redirects, so this promise never resolves in practice
39
- return new Promise(() => { });
40
- }
41
- // In iframe — listen for postMessage auth from parent
26
+ async login() {
27
+ const { verifier, challenge } = await generatePKCE();
28
+ const state = generateRandom(32);
29
+ // Store state + verifier for callback validation
30
+ sessionStorage.setItem("arinova_pkce_verifier", verifier);
31
+ sessionStorage.setItem("arinova_pkce_state", state);
32
+ const params = new URLSearchParams({
33
+ client_id: this.appId,
34
+ redirect_uri: this.redirectUri,
35
+ scope: this.scope,
36
+ state,
37
+ code_challenge: challenge,
38
+ code_challenge_method: "S256",
39
+ response_type: "code",
40
+ });
41
+ const authUrl = `${this.endpoint}/oauth/authorize?${params}`;
42
42
  return new Promise((resolve, reject) => {
43
- let settled = false;
44
- const timer = setTimeout(() => {
45
- if (!settled) {
46
- settled = true;
47
- window.removeEventListener("message", handler);
48
- reject(new Error("Arinova connect timeout: no auth message received from parent window within " + timeout + "ms"));
49
- }
50
- }, timeout);
51
- function handler(event) {
52
- if (event.data?.type !== "arinova:auth")
53
- return;
54
- if (settled)
55
- return;
56
- settled = true;
57
- clearTimeout(timer);
58
- window.removeEventListener("message", handler);
59
- const { user, accessToken, agents } = event.data.payload;
60
- resolve({ user, accessToken, agents: agents ?? [] });
43
+ const popup = window.open(authUrl, "arinova_auth", "width=500,height=600");
44
+ if (!popup) {
45
+ // Fallback: redirect instead of popup
46
+ window.location.href = authUrl;
47
+ reject(new Error("Popup blocked — redirecting instead"));
48
+ return;
61
49
  }
62
- window.addEventListener("message", handler);
50
+ const interval = setInterval(() => {
51
+ try {
52
+ if (popup.closed) {
53
+ clearInterval(interval);
54
+ reject(new Error("Login cancelled"));
55
+ }
56
+ const popupUrl = popup.location.href;
57
+ if (popupUrl.startsWith(this.redirectUri)) {
58
+ clearInterval(interval);
59
+ popup.close();
60
+ const url = new URL(popupUrl);
61
+ const code = url.searchParams.get("code");
62
+ const returnedState = url.searchParams.get("state");
63
+ if (returnedState !== state) {
64
+ reject(new Error("State mismatch — possible CSRF attack"));
65
+ return;
66
+ }
67
+ if (!code) {
68
+ reject(new Error(url.searchParams.get("error_description") ?? "No code received"));
69
+ return;
70
+ }
71
+ this.exchangeCode(code, verifier).then((token) => {
72
+ this.accessToken = token.access_token;
73
+ resolve(token);
74
+ }).catch(reject);
75
+ }
76
+ }
77
+ catch {
78
+ // Cross-origin — popup is on a different domain, ignore
79
+ }
80
+ }, 200);
81
+ // Timeout after 5 minutes
82
+ setTimeout(() => {
83
+ clearInterval(interval);
84
+ try {
85
+ popup.close();
86
+ }
87
+ catch { /* ignore */ }
88
+ reject(new Error("Login timed out"));
89
+ }, 300000);
63
90
  });
64
- },
91
+ }
65
92
  /**
66
- * Redirect to Arinova OAuth login page.
67
- * Call this from your game's frontend.
93
+ * Handle the OAuth callback (call this on your redirect_uri page).
94
+ * Reads code and state from URL, exchanges for token.
68
95
  */
69
- login(options) {
70
- const config = getConfig();
71
- const baseUrl = getBaseUrl();
72
- const scope = options?.scope?.join(" ") || "profile";
73
- // Store current URL for callback
74
- const currentUrl = typeof window !== "undefined" ? window.location.href : "";
75
- const params = new URLSearchParams({
76
- client_id: config.appId,
77
- redirect_uri: currentUrl,
78
- scope,
79
- state: crypto.randomUUID(),
96
+ async handleCallback() {
97
+ const url = new URL(window.location.href);
98
+ const code = url.searchParams.get("code");
99
+ const state = url.searchParams.get("state");
100
+ const verifier = sessionStorage.getItem("arinova_pkce_verifier");
101
+ const expectedState = sessionStorage.getItem("arinova_pkce_state");
102
+ sessionStorage.removeItem("arinova_pkce_verifier");
103
+ sessionStorage.removeItem("arinova_pkce_state");
104
+ if (!code) {
105
+ throw new Error(url.searchParams.get("error_description") ?? "No authorization code");
106
+ }
107
+ if (state !== expectedState) {
108
+ throw new Error("State mismatch");
109
+ }
110
+ if (!verifier) {
111
+ throw new Error("No PKCE verifier found — did you start login()?");
112
+ }
113
+ const token = await this.exchangeCode(code, verifier);
114
+ this.accessToken = token.access_token;
115
+ return token;
116
+ }
117
+ // ── Economy Methods ───────────────────────────────────────────
118
+ getToken() {
119
+ if (!this.accessToken) {
120
+ throw new Error("Not logged in — call login() or handleCallback() first");
121
+ }
122
+ return this.accessToken;
123
+ }
124
+ async apiFetch(path, init) {
125
+ const token = this.getToken();
126
+ const res = await fetch(`${this.endpoint}${path}`, {
127
+ ...init,
128
+ headers: {
129
+ "Content-Type": "application/json",
130
+ Authorization: `Bearer ${token}`,
131
+ ...init?.headers,
132
+ },
80
133
  });
81
- window.location.href = `${baseUrl}/oauth/authorize?${params}`;
82
- },
134
+ if (!res.ok) {
135
+ const body = await res.json().catch(() => ({}));
136
+ throw new Error(body.error_description ??
137
+ body.error ??
138
+ `API error (${res.status})`);
139
+ }
140
+ return res.json();
141
+ }
142
+ /** Get the current user's coin balance. */
143
+ async balance() {
144
+ return this.apiFetch("/api/v1/economy/balance");
145
+ }
83
146
  /**
84
- * Handle the OAuth callback. Call this on your redirect page.
85
- * Exchanges the authorization code for an access token.
147
+ * Purchase / charge coins from the user's balance.
148
+ * Requires the "economy" scope.
86
149
  */
87
- async handleCallback(params) {
88
- const baseUrl = getBaseUrl();
89
- const res = await fetch(`${baseUrl}/oauth/token`, {
150
+ async purchase(productId, amount, description) {
151
+ return this.apiFetch("/api/v1/economy/purchase", {
152
+ method: "POST",
153
+ body: JSON.stringify({ productId, amount, description }),
154
+ });
155
+ }
156
+ /** Get the user's transaction history. */
157
+ async transactions(limit, offset) {
158
+ const params = new URLSearchParams();
159
+ if (limit != null)
160
+ params.set("limit", String(limit));
161
+ if (offset != null)
162
+ params.set("offset", String(offset));
163
+ const qs = params.toString();
164
+ return this.apiFetch(`/api/v1/economy/transactions${qs ? `?${qs}` : ""}`);
165
+ }
166
+ async exchangeCode(code, codeVerifier) {
167
+ const res = await fetch(`${this.endpoint}/oauth/token`, {
90
168
  method: "POST",
91
169
  headers: { "Content-Type": "application/json" },
92
170
  body: JSON.stringify({
93
171
  grant_type: "authorization_code",
94
- code: params.code,
95
- client_id: params.clientId,
96
- client_secret: params.clientSecret,
97
- redirect_uri: params.redirectUri,
172
+ client_id: this.appId,
173
+ code,
174
+ redirect_uri: this.redirectUri,
175
+ code_verifier: codeVerifier,
98
176
  }),
99
177
  });
100
178
  if (!res.ok) {
101
- const error = await res.json().catch(() => ({ error: "token_exchange_failed" }));
102
- throw new Error(error.error || "Token exchange failed");
179
+ const body = await res.json().catch(() => ({}));
180
+ throw new Error(body.error_description ??
181
+ body.error ??
182
+ `Token exchange failed (${res.status})`);
103
183
  }
104
- const data = await res.json();
105
- return {
106
- user: data.user,
107
- accessToken: data.access_token,
108
- };
109
- },
110
- user: {
111
- /**
112
- * Get the authenticated user's profile.
113
- */
114
- async profile(accessToken) {
115
- const baseUrl = getBaseUrl();
116
- const res = await fetch(`${baseUrl}/api/v1/user/profile`, {
117
- headers: { Authorization: `Bearer ${accessToken}` },
118
- });
119
- if (!res.ok)
120
- throw new Error("Failed to get user profile");
121
- return res.json();
122
- },
123
- /**
124
- * Get the authenticated user's agents.
125
- * Requires "agents" scope.
126
- */
127
- async agents(accessToken) {
128
- const baseUrl = getBaseUrl();
129
- const res = await fetch(`${baseUrl}/api/v1/user/agents`, {
130
- headers: { Authorization: `Bearer ${accessToken}` },
131
- });
132
- if (!res.ok)
133
- throw new Error("Failed to get user agents");
134
- const data = await res.json();
135
- return data.agents;
136
- },
137
- },
138
- agent: {
139
- /**
140
- * Send a prompt to a user's agent and get a complete response.
141
- */
142
- async chat(options) {
143
- const baseUrl = getBaseUrl();
144
- const res = await fetch(`${baseUrl}/api/v1/agent/chat`, {
145
- method: "POST",
146
- headers: {
147
- "Content-Type": "application/json",
148
- Authorization: `Bearer ${options.accessToken}`,
149
- },
150
- body: JSON.stringify({
151
- agentId: options.agentId,
152
- prompt: options.prompt,
153
- systemPrompt: options.systemPrompt,
154
- }),
155
- });
156
- if (!res.ok) {
157
- const error = await res.json().catch(() => ({ error: "agent_chat_failed" }));
158
- throw new Error(error.error || "Agent chat failed");
159
- }
160
- return res.json();
161
- },
162
- /**
163
- * Send a prompt to a user's agent and receive a streaming response via SSE.
164
- */
165
- async chatStream(options) {
166
- const baseUrl = getBaseUrl();
167
- const res = await fetch(`${baseUrl}/api/v1/agent/chat/stream`, {
168
- method: "POST",
169
- headers: {
170
- "Content-Type": "application/json",
171
- Authorization: `Bearer ${options.accessToken}`,
172
- },
173
- body: JSON.stringify({
174
- agentId: options.agentId,
175
- prompt: options.prompt,
176
- systemPrompt: options.systemPrompt,
177
- }),
178
- });
179
- if (!res.ok) {
180
- const error = await res.json().catch(() => ({ error: "agent_stream_failed" }));
181
- throw new Error(error.error || "Agent stream failed");
182
- }
183
- const reader = res.body.getReader();
184
- const decoder = new TextDecoder();
185
- let fullContent = "";
186
- let buffer = "";
187
- while (true) {
188
- const { done, value } = await reader.read();
189
- if (done)
190
- break;
191
- buffer += decoder.decode(value, { stream: true });
192
- const lines = buffer.split("\n");
193
- buffer = lines.pop() || "";
194
- for (const line of lines) {
195
- if (!line.startsWith("data: "))
196
- continue;
197
- try {
198
- const event = JSON.parse(line.slice(6));
199
- if (event.type === "chunk") {
200
- options.onChunk(event.content);
201
- fullContent = event.content;
202
- }
203
- else if (event.type === "done") {
204
- fullContent = event.content;
205
- }
206
- else if (event.type === "error") {
207
- throw new Error(event.error);
208
- }
209
- }
210
- catch (e) {
211
- if (e instanceof Error && e.message !== "Unexpected end of JSON input")
212
- throw e;
213
- }
214
- }
215
- }
216
- return { content: fullContent, agentId: options.agentId };
217
- },
218
- },
219
- economy: {
220
- /**
221
- * Charge coins from a user's balance (server-to-server).
222
- * Requires clientId and clientSecret.
223
- */
224
- async charge(options) {
225
- const baseUrl = getBaseUrl();
226
- const res = await fetch(`${baseUrl}/api/v1/economy/charge`, {
227
- method: "POST",
228
- headers: {
229
- "Content-Type": "application/json",
230
- "X-Client-Id": options.clientId,
231
- "X-App-Secret": options.clientSecret,
232
- },
233
- body: JSON.stringify({
234
- userId: options.userId,
235
- amount: options.amount,
236
- description: options.description,
237
- }),
238
- });
239
- if (!res.ok) {
240
- const error = await res.json().catch(() => ({ error: "charge_failed" }));
241
- throw new Error(error.error || "Charge failed");
242
- }
243
- return res.json();
244
- },
245
- /**
246
- * Award coins to a user (server-to-server).
247
- * Requires clientId and clientSecret.
248
- */
249
- async award(options) {
250
- const baseUrl = getBaseUrl();
251
- const res = await fetch(`${baseUrl}/api/v1/economy/award`, {
252
- method: "POST",
253
- headers: {
254
- "Content-Type": "application/json",
255
- "X-Client-Id": options.clientId,
256
- "X-App-Secret": options.clientSecret,
257
- },
258
- body: JSON.stringify({
259
- userId: options.userId,
260
- amount: options.amount,
261
- description: options.description,
262
- }),
263
- });
264
- if (!res.ok) {
265
- const error = await res.json().catch(() => ({ error: "award_failed" }));
266
- throw new Error(error.error || "Award failed");
267
- }
268
- return res.json();
269
- },
270
- /**
271
- * Get a user's coin balance (uses OAuth access token).
272
- */
273
- async balance(accessToken) {
274
- const baseUrl = getBaseUrl();
275
- const res = await fetch(`${baseUrl}/api/v1/economy/balance`, {
276
- headers: { Authorization: `Bearer ${accessToken}` },
277
- });
278
- if (!res.ok)
279
- throw new Error("Failed to get balance");
280
- return res.json();
281
- },
282
- },
283
- };
284
- //# sourceMappingURL=index.js.map
184
+ return res.json();
185
+ }
186
+ }
187
+ // ── PKCE Helpers ──────────────────────────────────────────────
188
+ function generateRandom(length) {
189
+ const array = new Uint8Array(length);
190
+ crypto.getRandomValues(array);
191
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
192
+ }
193
+ async function generatePKCE() {
194
+ const verifier = generateRandom(32); // 64 hex chars
195
+ const encoder = new TextEncoder();
196
+ const data = encoder.encode(verifier);
197
+ const hash = await crypto.subtle.digest("SHA-256", data);
198
+ const challenge = base64UrlEncode(new Uint8Array(hash));
199
+ return { verifier, challenge };
200
+ }
201
+ function base64UrlEncode(buffer) {
202
+ let binary = "";
203
+ for (const byte of buffer) {
204
+ binary += String.fromCharCode(byte);
205
+ }
206
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
207
+ }
208
+ // Default export for convenience
209
+ export default Arinova;
package/dist/types.d.ts CHANGED
@@ -68,6 +68,28 @@ export interface AwardResponse {
68
68
  export interface BalanceResponse {
69
69
  balance: number;
70
70
  }
71
+ export interface PurchaseOptions {
72
+ productId: string;
73
+ amount: number;
74
+ description?: string;
75
+ }
76
+ export interface PurchaseResponse {
77
+ transactionId: string;
78
+ newBalance: number;
79
+ }
80
+ export interface TransactionRecord {
81
+ id: string;
82
+ type: string;
83
+ amount: number;
84
+ description: string | null;
85
+ createdAt: string;
86
+ }
87
+ export interface TransactionsResponse {
88
+ transactions: TransactionRecord[];
89
+ total: number;
90
+ limit: number;
91
+ offset: number;
92
+ }
71
93
  export interface SSEChunkEvent {
72
94
  type: "chunk";
73
95
  content: string;
@@ -81,4 +103,3 @@ export interface SSEErrorEvent {
81
103
  error: string;
82
104
  }
83
105
  export type SSEEvent = SSEChunkEvent | SSEDoneEvent | SSEErrorEvent;
84
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,20 +1,15 @@
1
1
  {
2
2
  "name": "@arinova-ai/spaces-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
+ "description": "Arinova Spaces SDK — OAuth PKCE login for third-party apps",
4
5
  "type": "module",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "import": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
11
- }
12
- },
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": ["dist"],
13
9
  "scripts": {
14
- "build": "tsc",
15
- "dev": "tsc --watch"
10
+ "build": "tsc"
16
11
  },
17
12
  "devDependencies": {
18
- "typescript": "^5.6.0"
13
+ "typescript": "^5.4.0"
19
14
  }
20
15
  }
@@ -1,4 +0,0 @@
1
-
2
- > @arinova-ai/spaces-sdk@0.0.2 build /Users/ripple/.openclaw/workspace/projects/arinova-chat/packages/spaces-sdk
3
- > tsc
4
-
package/src/index.ts DELETED
@@ -1,336 +0,0 @@
1
- import type {
2
- ArinovaConfig,
3
- LoginOptions,
4
- LoginResult,
5
- ConnectOptions,
6
- ConnectResult,
7
- ArinovaUser,
8
- AgentInfo,
9
- AgentChatOptions,
10
- AgentChatResponse,
11
- AgentChatStreamOptions,
12
- AgentChatStreamResponse,
13
- ChargeOptions,
14
- ChargeResponse,
15
- AwardOptions,
16
- AwardResponse,
17
- BalanceResponse,
18
- SSEEvent,
19
- } from "./types.js";
20
-
21
- export type * from "./types.js";
22
-
23
- let _config: ArinovaConfig | null = null;
24
- let _oauthClientInfo: { clientId: string; clientSecret: string } | null = null;
25
-
26
- function getBaseUrl(): string {
27
- if (!_config) throw new Error("Arinova SDK not initialized. Call Arinova.init() first.");
28
- return _config.baseUrl || "https://api.arinova.ai";
29
- }
30
-
31
- function getConfig(): ArinovaConfig {
32
- if (!_config) throw new Error("Arinova SDK not initialized. Call Arinova.init() first.");
33
- return _config;
34
- }
35
-
36
- export const Arinova = {
37
- /**
38
- * Initialize the SDK with your app configuration.
39
- */
40
- init(config: ArinovaConfig & { clientId?: string; clientSecret?: string }) {
41
- _config = config;
42
- if (config.clientId && config.clientSecret) {
43
- _oauthClientInfo = { clientId: config.clientId, clientSecret: config.clientSecret };
44
- }
45
- },
46
-
47
- /**
48
- * Connect to Arinova — works seamlessly in both iframe and standalone contexts.
49
- *
50
- * - **Inside an iframe** (embedded in Arinova Chat): receives auth via postMessage from the parent window.
51
- * - **Outside an iframe** (standalone): falls back to the OAuth login() flow.
52
- *
53
- * @param options.timeout - How long to wait for postMessage in iframe mode (default: 5000ms)
54
- * @returns Promise resolving with user, accessToken, and agents
55
- */
56
- async connect(options?: ConnectOptions): Promise<ConnectResult> {
57
- const timeout = options?.timeout ?? 5000;
58
-
59
- const inIframe = typeof window !== "undefined" && window.self !== window.top;
60
-
61
- if (!inIframe) {
62
- // Not in iframe — fall back to OAuth login flow
63
- this.login();
64
- // login() redirects, so this promise never resolves in practice
65
- return new Promise(() => {});
66
- }
67
-
68
- // In iframe — listen for postMessage auth from parent
69
- return new Promise<ConnectResult>((resolve, reject) => {
70
- let settled = false;
71
-
72
- const timer = setTimeout(() => {
73
- if (!settled) {
74
- settled = true;
75
- window.removeEventListener("message", handler);
76
- reject(new Error("Arinova connect timeout: no auth message received from parent window within " + timeout + "ms"));
77
- }
78
- }, timeout);
79
-
80
- function handler(event: MessageEvent) {
81
- if (event.data?.type !== "arinova:auth") return;
82
- if (settled) return;
83
- settled = true;
84
- clearTimeout(timer);
85
- window.removeEventListener("message", handler);
86
-
87
- const { user, accessToken, agents } = event.data.payload as ConnectResult;
88
- resolve({ user, accessToken, agents: agents ?? [] });
89
- }
90
-
91
- window.addEventListener("message", handler);
92
- });
93
- },
94
-
95
- /**
96
- * Redirect to Arinova OAuth login page.
97
- * Call this from your game's frontend.
98
- */
99
- login(options?: LoginOptions): void {
100
- const config = getConfig();
101
- const baseUrl = getBaseUrl();
102
- const scope = options?.scope?.join(" ") || "profile";
103
-
104
- // Store current URL for callback
105
- const currentUrl = typeof window !== "undefined" ? window.location.href : "";
106
-
107
- const params = new URLSearchParams({
108
- client_id: config.appId,
109
- redirect_uri: currentUrl,
110
- scope,
111
- state: crypto.randomUUID(),
112
- });
113
-
114
- window.location.href = `${baseUrl}/oauth/authorize?${params}`;
115
- },
116
-
117
- /**
118
- * Handle the OAuth callback. Call this on your redirect page.
119
- * Exchanges the authorization code for an access token.
120
- */
121
- async handleCallback(params: {
122
- code: string;
123
- clientId: string;
124
- clientSecret: string;
125
- redirectUri: string;
126
- }): Promise<LoginResult> {
127
- const baseUrl = getBaseUrl();
128
-
129
- const res = await fetch(`${baseUrl}/oauth/token`, {
130
- method: "POST",
131
- headers: { "Content-Type": "application/json" },
132
- body: JSON.stringify({
133
- grant_type: "authorization_code",
134
- code: params.code,
135
- client_id: params.clientId,
136
- client_secret: params.clientSecret,
137
- redirect_uri: params.redirectUri,
138
- }),
139
- });
140
-
141
- if (!res.ok) {
142
- const error = await res.json().catch(() => ({ error: "token_exchange_failed" }));
143
- throw new Error(error.error || "Token exchange failed");
144
- }
145
-
146
- const data = await res.json();
147
- return {
148
- user: data.user,
149
- accessToken: data.access_token,
150
- };
151
- },
152
-
153
- user: {
154
- /**
155
- * Get the authenticated user's profile.
156
- */
157
- async profile(accessToken: string): Promise<ArinovaUser> {
158
- const baseUrl = getBaseUrl();
159
- const res = await fetch(`${baseUrl}/api/v1/user/profile`, {
160
- headers: { Authorization: `Bearer ${accessToken}` },
161
- });
162
- if (!res.ok) throw new Error("Failed to get user profile");
163
- return res.json();
164
- },
165
-
166
- /**
167
- * Get the authenticated user's agents.
168
- * Requires "agents" scope.
169
- */
170
- async agents(accessToken: string): Promise<AgentInfo[]> {
171
- const baseUrl = getBaseUrl();
172
- const res = await fetch(`${baseUrl}/api/v1/user/agents`, {
173
- headers: { Authorization: `Bearer ${accessToken}` },
174
- });
175
- if (!res.ok) throw new Error("Failed to get user agents");
176
- const data = await res.json();
177
- return data.agents;
178
- },
179
- },
180
-
181
- agent: {
182
- /**
183
- * Send a prompt to a user's agent and get a complete response.
184
- */
185
- async chat(options: AgentChatOptions): Promise<AgentChatResponse> {
186
- const baseUrl = getBaseUrl();
187
- const res = await fetch(`${baseUrl}/api/v1/agent/chat`, {
188
- method: "POST",
189
- headers: {
190
- "Content-Type": "application/json",
191
- Authorization: `Bearer ${options.accessToken}`,
192
- },
193
- body: JSON.stringify({
194
- agentId: options.agentId,
195
- prompt: options.prompt,
196
- systemPrompt: options.systemPrompt,
197
- }),
198
- });
199
-
200
- if (!res.ok) {
201
- const error = await res.json().catch(() => ({ error: "agent_chat_failed" }));
202
- throw new Error(error.error || "Agent chat failed");
203
- }
204
-
205
- return res.json();
206
- },
207
-
208
- /**
209
- * Send a prompt to a user's agent and receive a streaming response via SSE.
210
- */
211
- async chatStream(options: AgentChatStreamOptions): Promise<AgentChatStreamResponse> {
212
- const baseUrl = getBaseUrl();
213
- const res = await fetch(`${baseUrl}/api/v1/agent/chat/stream`, {
214
- method: "POST",
215
- headers: {
216
- "Content-Type": "application/json",
217
- Authorization: `Bearer ${options.accessToken}`,
218
- },
219
- body: JSON.stringify({
220
- agentId: options.agentId,
221
- prompt: options.prompt,
222
- systemPrompt: options.systemPrompt,
223
- }),
224
- });
225
-
226
- if (!res.ok) {
227
- const error = await res.json().catch(() => ({ error: "agent_stream_failed" }));
228
- throw new Error(error.error || "Agent stream failed");
229
- }
230
-
231
- const reader = res.body!.getReader();
232
- const decoder = new TextDecoder();
233
- let fullContent = "";
234
- let buffer = "";
235
-
236
- while (true) {
237
- const { done, value } = await reader.read();
238
- if (done) break;
239
-
240
- buffer += decoder.decode(value, { stream: true });
241
- const lines = buffer.split("\n");
242
- buffer = lines.pop() || "";
243
-
244
- for (const line of lines) {
245
- if (!line.startsWith("data: ")) continue;
246
- try {
247
- const event: SSEEvent = JSON.parse(line.slice(6));
248
- if (event.type === "chunk") {
249
- options.onChunk(event.content);
250
- fullContent = event.content;
251
- } else if (event.type === "done") {
252
- fullContent = event.content;
253
- } else if (event.type === "error") {
254
- throw new Error(event.error);
255
- }
256
- } catch (e) {
257
- if (e instanceof Error && e.message !== "Unexpected end of JSON input") throw e;
258
- }
259
- }
260
- }
261
-
262
- return { content: fullContent, agentId: options.agentId };
263
- },
264
- },
265
-
266
- economy: {
267
- /**
268
- * Charge coins from a user's balance (server-to-server).
269
- * Requires clientId and clientSecret.
270
- */
271
- async charge(options: ChargeOptions & { clientId: string; clientSecret: string }): Promise<ChargeResponse> {
272
- const baseUrl = getBaseUrl();
273
- const res = await fetch(`${baseUrl}/api/v1/economy/charge`, {
274
- method: "POST",
275
- headers: {
276
- "Content-Type": "application/json",
277
- "X-Client-Id": options.clientId,
278
- "X-App-Secret": options.clientSecret,
279
- },
280
- body: JSON.stringify({
281
- userId: options.userId,
282
- amount: options.amount,
283
- description: options.description,
284
- }),
285
- });
286
-
287
- if (!res.ok) {
288
- const error = await res.json().catch(() => ({ error: "charge_failed" }));
289
- throw new Error(error.error || "Charge failed");
290
- }
291
-
292
- return res.json();
293
- },
294
-
295
- /**
296
- * Award coins to a user (server-to-server).
297
- * Requires clientId and clientSecret.
298
- */
299
- async award(options: AwardOptions & { clientId: string; clientSecret: string }): Promise<AwardResponse> {
300
- const baseUrl = getBaseUrl();
301
- const res = await fetch(`${baseUrl}/api/v1/economy/award`, {
302
- method: "POST",
303
- headers: {
304
- "Content-Type": "application/json",
305
- "X-Client-Id": options.clientId,
306
- "X-App-Secret": options.clientSecret,
307
- },
308
- body: JSON.stringify({
309
- userId: options.userId,
310
- amount: options.amount,
311
- description: options.description,
312
- }),
313
- });
314
-
315
- if (!res.ok) {
316
- const error = await res.json().catch(() => ({ error: "award_failed" }));
317
- throw new Error(error.error || "Award failed");
318
- }
319
-
320
- return res.json();
321
- },
322
-
323
- /**
324
- * Get a user's coin balance (uses OAuth access token).
325
- */
326
- async balance(accessToken: string): Promise<BalanceResponse> {
327
- const baseUrl = getBaseUrl();
328
- const res = await fetch(`${baseUrl}/api/v1/economy/balance`, {
329
- headers: { Authorization: `Bearer ${accessToken}` },
330
- });
331
-
332
- if (!res.ok) throw new Error("Failed to get balance");
333
- return res.json();
334
- },
335
- },
336
- };
package/src/types.ts DELETED
@@ -1,107 +0,0 @@
1
- // ===== Config =====
2
- export interface ArinovaConfig {
3
- appId: string;
4
- baseUrl?: string; // defaults to "https://api.arinova.ai"
5
- }
6
-
7
- // ===== Auth =====
8
- export interface LoginOptions {
9
- scope?: string[]; // defaults to ["profile"]
10
- }
11
-
12
- export interface LoginResult {
13
- user: ArinovaUser;
14
- accessToken: string;
15
- }
16
-
17
- export interface ConnectOptions {
18
- timeout?: number; // milliseconds, defaults to 5000
19
- }
20
-
21
- export interface ConnectResult {
22
- user: ArinovaUser;
23
- accessToken: string;
24
- agents: AgentInfo[];
25
- }
26
-
27
- export interface ArinovaUser {
28
- id: string;
29
- name: string;
30
- email: string;
31
- image: string | null;
32
- }
33
-
34
- // ===== Agent =====
35
- export interface AgentInfo {
36
- id: string;
37
- name: string;
38
- description: string | null;
39
- avatarUrl: string | null;
40
- }
41
-
42
- export interface AgentChatOptions {
43
- agentId: string;
44
- prompt: string;
45
- systemPrompt?: string;
46
- accessToken: string;
47
- }
48
-
49
- export interface AgentChatResponse {
50
- response: string;
51
- agentId: string;
52
- }
53
-
54
- export interface AgentChatStreamOptions extends AgentChatOptions {
55
- onChunk: (chunk: string) => void;
56
- }
57
-
58
- export interface AgentChatStreamResponse {
59
- content: string;
60
- agentId: string;
61
- }
62
-
63
- // ===== Economy =====
64
- export interface ChargeOptions {
65
- userId: string;
66
- amount: number;
67
- description?: string;
68
- }
69
-
70
- export interface ChargeResponse {
71
- transactionId: string;
72
- newBalance: number;
73
- }
74
-
75
- export interface AwardOptions {
76
- userId: string;
77
- amount: number;
78
- description?: string;
79
- }
80
-
81
- export interface AwardResponse {
82
- transactionId: string;
83
- newBalance: number;
84
- platformFee: number;
85
- }
86
-
87
- export interface BalanceResponse {
88
- balance: number;
89
- }
90
-
91
- // ===== SSE Event =====
92
- export interface SSEChunkEvent {
93
- type: "chunk";
94
- content: string;
95
- }
96
-
97
- export interface SSEDoneEvent {
98
- type: "done";
99
- content: string;
100
- }
101
-
102
- export interface SSEErrorEvent {
103
- type: "error";
104
- error: string;
105
- }
106
-
107
- export type SSEEvent = SSEChunkEvent | SSEDoneEvent | SSEErrorEvent;
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022", "DOM"],
7
- "outDir": "./dist",
8
- "rootDir": "./src",
9
- "declaration": true,
10
- "declarationMap": true,
11
- "sourceMap": true,
12
- "strict": true,
13
- "esModuleInterop": true,
14
- "skipLibCheck": true,
15
- "forceConsistentCasingInFileNames": true
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist"]
19
- }