spaps-sdk 1.1.52 → 1.2.1
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 +10 -3
- package/dist/index.d.mts +158 -7
- package/dist/index.d.ts +158 -7
- package/dist/index.js +204 -9
- package/dist/index.mjs +201 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -189,14 +189,21 @@ const user = await spaps.getUser();
|
|
|
189
189
|
|
|
190
190
|
### 💳 Stripe Integration
|
|
191
191
|
```javascript
|
|
192
|
-
// Create checkout session
|
|
193
|
-
const session = await spaps.
|
|
194
|
-
|
|
192
|
+
// Create checkout session with consent control
|
|
193
|
+
const session = await spaps.payments.createPaymentCheckout({
|
|
194
|
+
price_id: 'price_123',
|
|
195
|
+
success_url: 'https://app.example.com/success',
|
|
196
|
+
cancel_url: 'https://app.example.com/cancel',
|
|
197
|
+
require_legal_consent: true,
|
|
198
|
+
legal_consent_text: 'I agree I am 18+ and accept the HTMA Terms & Privacy.'
|
|
199
|
+
});
|
|
200
|
+
window.location.href = session.url;
|
|
195
201
|
|
|
196
202
|
// Manage subscription
|
|
197
203
|
const subscription = await spaps.getSubscription();
|
|
198
204
|
await spaps.cancelSubscription();
|
|
199
205
|
```
|
|
206
|
+
> `require_legal_consent` forces Stripe’s Terms/Privacy checkbox, and `legal_consent_text` (≤120 chars) customizes the copy. Text is sanitized and defaults to “I agree to the Terms of Service and Privacy Policy.” when omitted.
|
|
200
207
|
|
|
201
208
|
### 📊 Usage Tracking
|
|
202
209
|
```javascript
|
package/dist/index.d.mts
CHANGED
|
@@ -85,12 +85,53 @@ declare class PermissionChecker {
|
|
|
85
85
|
declare function createPermissionChecker(customAdmins?: (string | AdminConfig)[]): PermissionChecker;
|
|
86
86
|
declare const defaultPermissionChecker: PermissionChecker;
|
|
87
87
|
|
|
88
|
+
type ApiKeyType = 'publishable' | 'secret';
|
|
88
89
|
interface SPAPSConfig {
|
|
89
90
|
apiUrl?: string;
|
|
91
|
+
/** @deprecated Use publishableKey or secretKey instead */
|
|
90
92
|
apiKey?: string;
|
|
93
|
+
/** Browser-safe key for client-side usage (spaps_pub_xxx) */
|
|
94
|
+
publishableKey?: string;
|
|
95
|
+
/** Server-only key for full access (spaps_sec_xxx) */
|
|
96
|
+
secretKey?: string;
|
|
91
97
|
autoDetect?: boolean;
|
|
92
98
|
timeout?: number;
|
|
93
99
|
}
|
|
100
|
+
interface CheckoutLineItemPriceData {
|
|
101
|
+
currency: string;
|
|
102
|
+
unit_amount: number;
|
|
103
|
+
product_data: {
|
|
104
|
+
name: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
metadata?: Record<string, string>;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
interface CheckoutLineItem {
|
|
110
|
+
price_id?: string;
|
|
111
|
+
product_id?: string;
|
|
112
|
+
quantity: number;
|
|
113
|
+
price_data?: CheckoutLineItemPriceData;
|
|
114
|
+
}
|
|
115
|
+
interface CreateCheckoutSessionPayload {
|
|
116
|
+
mode: 'payment' | 'subscription';
|
|
117
|
+
line_items: CheckoutLineItem[];
|
|
118
|
+
success_url: string;
|
|
119
|
+
cancel_url: string;
|
|
120
|
+
metadata?: Record<string, string>;
|
|
121
|
+
customer_email?: string;
|
|
122
|
+
client_reference_id?: string;
|
|
123
|
+
payment_intent_data?: {
|
|
124
|
+
metadata?: Record<string, string>;
|
|
125
|
+
};
|
|
126
|
+
subscription_data?: {
|
|
127
|
+
metadata?: Record<string, string>;
|
|
128
|
+
trial_period_days?: number;
|
|
129
|
+
};
|
|
130
|
+
allow_promotion_codes?: boolean;
|
|
131
|
+
locale?: string;
|
|
132
|
+
require_legal_consent?: boolean;
|
|
133
|
+
legal_consent_text?: string;
|
|
134
|
+
}
|
|
94
135
|
|
|
95
136
|
declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Record<string, any>> {
|
|
96
137
|
private client;
|
|
@@ -203,14 +244,13 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
203
244
|
password: string;
|
|
204
245
|
}) => Promise<any>;
|
|
205
246
|
/**
|
|
206
|
-
* Verify a magic link token
|
|
207
|
-
*
|
|
247
|
+
* Verify a magic link token and authenticate the user.
|
|
248
|
+
* Returns full auth response with tokens and auto-stores them.
|
|
208
249
|
*/
|
|
209
250
|
verifyMagicLink: (payload: {
|
|
210
251
|
token: string;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}>;
|
|
252
|
+
type?: string;
|
|
253
|
+
}) => Promise<AuthResponse>;
|
|
214
254
|
solana: {
|
|
215
255
|
linkWallet: (payload: {
|
|
216
256
|
wallet_address: string;
|
|
@@ -269,18 +309,22 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
269
309
|
cursor?: Record<string, unknown>;
|
|
270
310
|
}>;
|
|
271
311
|
};
|
|
272
|
-
createCheckoutSession: (payload:
|
|
312
|
+
createCheckoutSession: (payload: CreateCheckoutSessionPayload) => Promise<CheckoutSession>;
|
|
273
313
|
createPaymentCheckout: (params: {
|
|
274
314
|
price_id: string;
|
|
275
315
|
quantity?: number;
|
|
276
316
|
success_url: string;
|
|
277
317
|
cancel_url: string;
|
|
318
|
+
require_legal_consent?: boolean;
|
|
319
|
+
legal_consent_text?: string;
|
|
278
320
|
}) => Promise<CheckoutSession>;
|
|
279
321
|
createSubscriptionCheckout: (params: {
|
|
280
322
|
price_id: string;
|
|
281
323
|
success_url: string;
|
|
282
324
|
cancel_url: string;
|
|
283
325
|
trial_period_days?: number;
|
|
326
|
+
require_legal_consent?: boolean;
|
|
327
|
+
legal_consent_text?: string;
|
|
284
328
|
}) => Promise<CheckoutSession>;
|
|
285
329
|
getCheckoutSession: (sessionId: string) => Promise<CheckoutSession>;
|
|
286
330
|
listCheckoutSessions: (query?: {
|
|
@@ -357,6 +401,51 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
357
401
|
revokeAll: () => Promise<any>;
|
|
358
402
|
touch: () => Promise<any>;
|
|
359
403
|
};
|
|
404
|
+
/**
|
|
405
|
+
* CFO (QuickBooks) namespace for managing multi-account connections
|
|
406
|
+
*/
|
|
407
|
+
cfo: {
|
|
408
|
+
/**
|
|
409
|
+
* Get all QuickBooks connections for the current user
|
|
410
|
+
*/
|
|
411
|
+
getConnections: () => Promise<{
|
|
412
|
+
connections: Array<{
|
|
413
|
+
id: string;
|
|
414
|
+
status: string;
|
|
415
|
+
companyName?: string;
|
|
416
|
+
realmId?: string;
|
|
417
|
+
connectedAt?: string;
|
|
418
|
+
}>;
|
|
419
|
+
}>;
|
|
420
|
+
/**
|
|
421
|
+
* Get connection status (returns primary connection for backward compatibility)
|
|
422
|
+
*/
|
|
423
|
+
getStatus: () => Promise<{
|
|
424
|
+
connection: {
|
|
425
|
+
id: string;
|
|
426
|
+
status: string;
|
|
427
|
+
companyName?: string;
|
|
428
|
+
realmId?: string;
|
|
429
|
+
} | null;
|
|
430
|
+
totalConnections: number;
|
|
431
|
+
}>;
|
|
432
|
+
/**
|
|
433
|
+
* Start adding a new QuickBooks connection
|
|
434
|
+
* Returns auth URL to redirect user to QuickBooks OAuth
|
|
435
|
+
*/
|
|
436
|
+
addConnection: () => Promise<{
|
|
437
|
+
authUrl: string;
|
|
438
|
+
connectionId: string;
|
|
439
|
+
existingConnections: number;
|
|
440
|
+
}>;
|
|
441
|
+
/**
|
|
442
|
+
* Disconnect a specific QuickBooks account
|
|
443
|
+
*/
|
|
444
|
+
disconnect: (connectionId: string) => Promise<{
|
|
445
|
+
success: boolean;
|
|
446
|
+
message: string;
|
|
447
|
+
}>;
|
|
448
|
+
};
|
|
360
449
|
createCheckoutSession(priceId: string, successUrl: string, cancelUrl?: string): Promise<{
|
|
361
450
|
data: CheckoutSession;
|
|
362
451
|
}>;
|
|
@@ -417,6 +506,12 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
417
506
|
isAuthenticated(): boolean;
|
|
418
507
|
getAccessToken(): string | undefined;
|
|
419
508
|
setAccessToken(token: string): void;
|
|
509
|
+
setRefreshToken(token: string): void;
|
|
510
|
+
/**
|
|
511
|
+
* Restore tokens (for SSO/OAuth flows)
|
|
512
|
+
* Sets both access and refresh tokens
|
|
513
|
+
*/
|
|
514
|
+
restoreTokens(accessToken: string, refreshToken?: string): void;
|
|
420
515
|
isLocalMode(): boolean;
|
|
421
516
|
/**
|
|
422
517
|
* Check if current user has admin privileges
|
|
@@ -433,6 +528,10 @@ declare class TokenManager {
|
|
|
433
528
|
private static readonly ACCESS_TOKEN_KEY;
|
|
434
529
|
private static readonly REFRESH_TOKEN_KEY;
|
|
435
530
|
private static readonly USER_KEY;
|
|
531
|
+
/**
|
|
532
|
+
* Cross-platform base64 decode (works in both Node.js and browser)
|
|
533
|
+
*/
|
|
534
|
+
private static base64Decode;
|
|
436
535
|
private static getStorage;
|
|
437
536
|
static storeTokens(tokens: AuthResponse): void;
|
|
438
537
|
static getAccessToken(): string | null;
|
|
@@ -441,10 +540,62 @@ declare class TokenManager {
|
|
|
441
540
|
static clearTokens(): void;
|
|
442
541
|
static isTokenExpired(token: string): boolean;
|
|
443
542
|
static autoRefreshToken(sdk: SPAPSClient): Promise<boolean>;
|
|
543
|
+
/**
|
|
544
|
+
* Parse auth tokens from URL fragment (for OAuth callbacks)
|
|
545
|
+
* URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
|
|
546
|
+
*/
|
|
547
|
+
static parseTokensFromUrlFragment(url?: string): {
|
|
548
|
+
access_token?: string;
|
|
549
|
+
refresh_token?: string;
|
|
550
|
+
user_id?: string;
|
|
551
|
+
} | null;
|
|
552
|
+
/**
|
|
553
|
+
* Handle OAuth callback - parse tokens from URL and store them
|
|
554
|
+
* Returns user info decoded from JWT, or null if no tokens found
|
|
555
|
+
*/
|
|
556
|
+
static handleOAuthCallback(sdk: SPAPSClient): {
|
|
557
|
+
id: string;
|
|
558
|
+
email?: string;
|
|
559
|
+
role?: string;
|
|
560
|
+
} | null;
|
|
444
561
|
}
|
|
445
562
|
declare class WalletUtils {
|
|
446
563
|
static detectChainType(address: string): 'solana' | 'ethereum' | 'bitcoin' | null;
|
|
447
564
|
static isValidAddress(address: string, chainType?: 'solana' | 'ethereum' | 'bitcoin' | 'base'): boolean;
|
|
448
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Create a SPAPS client for browser/client-side usage
|
|
568
|
+
* Uses publishable key which is safe to expose in client bundles
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ```typescript
|
|
572
|
+
* // In your frontend code
|
|
573
|
+
* const spaps = createBrowserClient('spaps_pub_xxx');
|
|
574
|
+
*
|
|
575
|
+
* // Use for authentication and checkout
|
|
576
|
+
* const { user } = await spaps.auth.signInWithPassword({ email, password });
|
|
577
|
+
* const checkout = await spaps.payments.createCheckoutSession({...});
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
declare function createBrowserClient(publishableKey: string, options?: Omit<SPAPSConfig, 'publishableKey' | 'secretKey' | 'apiKey'>): SPAPSClient;
|
|
581
|
+
/**
|
|
582
|
+
* Create a SPAPS client for server-side usage
|
|
583
|
+
* Uses secret key which provides full access to all endpoints
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```typescript
|
|
587
|
+
* // In your API routes (Next.js, Express, etc.)
|
|
588
|
+
* const spaps = createServerClient('spaps_sec_xxx');
|
|
589
|
+
*
|
|
590
|
+
* // Full access to admin operations
|
|
591
|
+
* await spaps.admin.createProduct({...});
|
|
592
|
+
* await spaps.payments.crypto.reconcile();
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
declare function createServerClient(secretKey: string, options?: Omit<SPAPSConfig, 'publishableKey' | 'secretKey' | 'apiKey'>): SPAPSClient;
|
|
596
|
+
/**
|
|
597
|
+
* Detect key type from key prefix
|
|
598
|
+
*/
|
|
599
|
+
declare function detectKeyType(key: string): ApiKeyType | null;
|
|
449
600
|
|
|
450
|
-
export { type AdminConfig, DEFAULT_ADMIN_ACCOUNTS, type PermissionCheckResult, PermissionChecker, SPAPSClient as SPAPS, SPAPSClient, type SPAPSConfig, TokenManager, WalletUtils, canAccessAdmin, createPermissionChecker, SPAPSClient as default, defaultPermissionChecker, getRoleAwareErrorMessage, getUserDisplay, getUserRole, hasPermission, isAdminAccount, verifyCryptoWebhookSignature };
|
|
601
|
+
export { type AdminConfig, type ApiKeyType, type CheckoutLineItem, type CheckoutLineItemPriceData, type CreateCheckoutSessionPayload, DEFAULT_ADMIN_ACCOUNTS, type PermissionCheckResult, PermissionChecker, SPAPSClient as SPAPS, SPAPSClient, type SPAPSConfig, TokenManager, WalletUtils, canAccessAdmin, createBrowserClient, createPermissionChecker, createServerClient, SPAPSClient as default, defaultPermissionChecker, detectKeyType, getRoleAwareErrorMessage, getUserDisplay, getUserRole, hasPermission, isAdminAccount, verifyCryptoWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -85,12 +85,53 @@ declare class PermissionChecker {
|
|
|
85
85
|
declare function createPermissionChecker(customAdmins?: (string | AdminConfig)[]): PermissionChecker;
|
|
86
86
|
declare const defaultPermissionChecker: PermissionChecker;
|
|
87
87
|
|
|
88
|
+
type ApiKeyType = 'publishable' | 'secret';
|
|
88
89
|
interface SPAPSConfig {
|
|
89
90
|
apiUrl?: string;
|
|
91
|
+
/** @deprecated Use publishableKey or secretKey instead */
|
|
90
92
|
apiKey?: string;
|
|
93
|
+
/** Browser-safe key for client-side usage (spaps_pub_xxx) */
|
|
94
|
+
publishableKey?: string;
|
|
95
|
+
/** Server-only key for full access (spaps_sec_xxx) */
|
|
96
|
+
secretKey?: string;
|
|
91
97
|
autoDetect?: boolean;
|
|
92
98
|
timeout?: number;
|
|
93
99
|
}
|
|
100
|
+
interface CheckoutLineItemPriceData {
|
|
101
|
+
currency: string;
|
|
102
|
+
unit_amount: number;
|
|
103
|
+
product_data: {
|
|
104
|
+
name: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
metadata?: Record<string, string>;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
interface CheckoutLineItem {
|
|
110
|
+
price_id?: string;
|
|
111
|
+
product_id?: string;
|
|
112
|
+
quantity: number;
|
|
113
|
+
price_data?: CheckoutLineItemPriceData;
|
|
114
|
+
}
|
|
115
|
+
interface CreateCheckoutSessionPayload {
|
|
116
|
+
mode: 'payment' | 'subscription';
|
|
117
|
+
line_items: CheckoutLineItem[];
|
|
118
|
+
success_url: string;
|
|
119
|
+
cancel_url: string;
|
|
120
|
+
metadata?: Record<string, string>;
|
|
121
|
+
customer_email?: string;
|
|
122
|
+
client_reference_id?: string;
|
|
123
|
+
payment_intent_data?: {
|
|
124
|
+
metadata?: Record<string, string>;
|
|
125
|
+
};
|
|
126
|
+
subscription_data?: {
|
|
127
|
+
metadata?: Record<string, string>;
|
|
128
|
+
trial_period_days?: number;
|
|
129
|
+
};
|
|
130
|
+
allow_promotion_codes?: boolean;
|
|
131
|
+
locale?: string;
|
|
132
|
+
require_legal_consent?: boolean;
|
|
133
|
+
legal_consent_text?: string;
|
|
134
|
+
}
|
|
94
135
|
|
|
95
136
|
declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Record<string, any>> {
|
|
96
137
|
private client;
|
|
@@ -203,14 +244,13 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
203
244
|
password: string;
|
|
204
245
|
}) => Promise<any>;
|
|
205
246
|
/**
|
|
206
|
-
* Verify a magic link token
|
|
207
|
-
*
|
|
247
|
+
* Verify a magic link token and authenticate the user.
|
|
248
|
+
* Returns full auth response with tokens and auto-stores them.
|
|
208
249
|
*/
|
|
209
250
|
verifyMagicLink: (payload: {
|
|
210
251
|
token: string;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}>;
|
|
252
|
+
type?: string;
|
|
253
|
+
}) => Promise<AuthResponse>;
|
|
214
254
|
solana: {
|
|
215
255
|
linkWallet: (payload: {
|
|
216
256
|
wallet_address: string;
|
|
@@ -269,18 +309,22 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
269
309
|
cursor?: Record<string, unknown>;
|
|
270
310
|
}>;
|
|
271
311
|
};
|
|
272
|
-
createCheckoutSession: (payload:
|
|
312
|
+
createCheckoutSession: (payload: CreateCheckoutSessionPayload) => Promise<CheckoutSession>;
|
|
273
313
|
createPaymentCheckout: (params: {
|
|
274
314
|
price_id: string;
|
|
275
315
|
quantity?: number;
|
|
276
316
|
success_url: string;
|
|
277
317
|
cancel_url: string;
|
|
318
|
+
require_legal_consent?: boolean;
|
|
319
|
+
legal_consent_text?: string;
|
|
278
320
|
}) => Promise<CheckoutSession>;
|
|
279
321
|
createSubscriptionCheckout: (params: {
|
|
280
322
|
price_id: string;
|
|
281
323
|
success_url: string;
|
|
282
324
|
cancel_url: string;
|
|
283
325
|
trial_period_days?: number;
|
|
326
|
+
require_legal_consent?: boolean;
|
|
327
|
+
legal_consent_text?: string;
|
|
284
328
|
}) => Promise<CheckoutSession>;
|
|
285
329
|
getCheckoutSession: (sessionId: string) => Promise<CheckoutSession>;
|
|
286
330
|
listCheckoutSessions: (query?: {
|
|
@@ -357,6 +401,51 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
357
401
|
revokeAll: () => Promise<any>;
|
|
358
402
|
touch: () => Promise<any>;
|
|
359
403
|
};
|
|
404
|
+
/**
|
|
405
|
+
* CFO (QuickBooks) namespace for managing multi-account connections
|
|
406
|
+
*/
|
|
407
|
+
cfo: {
|
|
408
|
+
/**
|
|
409
|
+
* Get all QuickBooks connections for the current user
|
|
410
|
+
*/
|
|
411
|
+
getConnections: () => Promise<{
|
|
412
|
+
connections: Array<{
|
|
413
|
+
id: string;
|
|
414
|
+
status: string;
|
|
415
|
+
companyName?: string;
|
|
416
|
+
realmId?: string;
|
|
417
|
+
connectedAt?: string;
|
|
418
|
+
}>;
|
|
419
|
+
}>;
|
|
420
|
+
/**
|
|
421
|
+
* Get connection status (returns primary connection for backward compatibility)
|
|
422
|
+
*/
|
|
423
|
+
getStatus: () => Promise<{
|
|
424
|
+
connection: {
|
|
425
|
+
id: string;
|
|
426
|
+
status: string;
|
|
427
|
+
companyName?: string;
|
|
428
|
+
realmId?: string;
|
|
429
|
+
} | null;
|
|
430
|
+
totalConnections: number;
|
|
431
|
+
}>;
|
|
432
|
+
/**
|
|
433
|
+
* Start adding a new QuickBooks connection
|
|
434
|
+
* Returns auth URL to redirect user to QuickBooks OAuth
|
|
435
|
+
*/
|
|
436
|
+
addConnection: () => Promise<{
|
|
437
|
+
authUrl: string;
|
|
438
|
+
connectionId: string;
|
|
439
|
+
existingConnections: number;
|
|
440
|
+
}>;
|
|
441
|
+
/**
|
|
442
|
+
* Disconnect a specific QuickBooks account
|
|
443
|
+
*/
|
|
444
|
+
disconnect: (connectionId: string) => Promise<{
|
|
445
|
+
success: boolean;
|
|
446
|
+
message: string;
|
|
447
|
+
}>;
|
|
448
|
+
};
|
|
360
449
|
createCheckoutSession(priceId: string, successUrl: string, cancelUrl?: string): Promise<{
|
|
361
450
|
data: CheckoutSession;
|
|
362
451
|
}>;
|
|
@@ -417,6 +506,12 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
|
|
|
417
506
|
isAuthenticated(): boolean;
|
|
418
507
|
getAccessToken(): string | undefined;
|
|
419
508
|
setAccessToken(token: string): void;
|
|
509
|
+
setRefreshToken(token: string): void;
|
|
510
|
+
/**
|
|
511
|
+
* Restore tokens (for SSO/OAuth flows)
|
|
512
|
+
* Sets both access and refresh tokens
|
|
513
|
+
*/
|
|
514
|
+
restoreTokens(accessToken: string, refreshToken?: string): void;
|
|
420
515
|
isLocalMode(): boolean;
|
|
421
516
|
/**
|
|
422
517
|
* Check if current user has admin privileges
|
|
@@ -433,6 +528,10 @@ declare class TokenManager {
|
|
|
433
528
|
private static readonly ACCESS_TOKEN_KEY;
|
|
434
529
|
private static readonly REFRESH_TOKEN_KEY;
|
|
435
530
|
private static readonly USER_KEY;
|
|
531
|
+
/**
|
|
532
|
+
* Cross-platform base64 decode (works in both Node.js and browser)
|
|
533
|
+
*/
|
|
534
|
+
private static base64Decode;
|
|
436
535
|
private static getStorage;
|
|
437
536
|
static storeTokens(tokens: AuthResponse): void;
|
|
438
537
|
static getAccessToken(): string | null;
|
|
@@ -441,10 +540,62 @@ declare class TokenManager {
|
|
|
441
540
|
static clearTokens(): void;
|
|
442
541
|
static isTokenExpired(token: string): boolean;
|
|
443
542
|
static autoRefreshToken(sdk: SPAPSClient): Promise<boolean>;
|
|
543
|
+
/**
|
|
544
|
+
* Parse auth tokens from URL fragment (for OAuth callbacks)
|
|
545
|
+
* URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
|
|
546
|
+
*/
|
|
547
|
+
static parseTokensFromUrlFragment(url?: string): {
|
|
548
|
+
access_token?: string;
|
|
549
|
+
refresh_token?: string;
|
|
550
|
+
user_id?: string;
|
|
551
|
+
} | null;
|
|
552
|
+
/**
|
|
553
|
+
* Handle OAuth callback - parse tokens from URL and store them
|
|
554
|
+
* Returns user info decoded from JWT, or null if no tokens found
|
|
555
|
+
*/
|
|
556
|
+
static handleOAuthCallback(sdk: SPAPSClient): {
|
|
557
|
+
id: string;
|
|
558
|
+
email?: string;
|
|
559
|
+
role?: string;
|
|
560
|
+
} | null;
|
|
444
561
|
}
|
|
445
562
|
declare class WalletUtils {
|
|
446
563
|
static detectChainType(address: string): 'solana' | 'ethereum' | 'bitcoin' | null;
|
|
447
564
|
static isValidAddress(address: string, chainType?: 'solana' | 'ethereum' | 'bitcoin' | 'base'): boolean;
|
|
448
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Create a SPAPS client for browser/client-side usage
|
|
568
|
+
* Uses publishable key which is safe to expose in client bundles
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ```typescript
|
|
572
|
+
* // In your frontend code
|
|
573
|
+
* const spaps = createBrowserClient('spaps_pub_xxx');
|
|
574
|
+
*
|
|
575
|
+
* // Use for authentication and checkout
|
|
576
|
+
* const { user } = await spaps.auth.signInWithPassword({ email, password });
|
|
577
|
+
* const checkout = await spaps.payments.createCheckoutSession({...});
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
declare function createBrowserClient(publishableKey: string, options?: Omit<SPAPSConfig, 'publishableKey' | 'secretKey' | 'apiKey'>): SPAPSClient;
|
|
581
|
+
/**
|
|
582
|
+
* Create a SPAPS client for server-side usage
|
|
583
|
+
* Uses secret key which provides full access to all endpoints
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```typescript
|
|
587
|
+
* // In your API routes (Next.js, Express, etc.)
|
|
588
|
+
* const spaps = createServerClient('spaps_sec_xxx');
|
|
589
|
+
*
|
|
590
|
+
* // Full access to admin operations
|
|
591
|
+
* await spaps.admin.createProduct({...});
|
|
592
|
+
* await spaps.payments.crypto.reconcile();
|
|
593
|
+
* ```
|
|
594
|
+
*/
|
|
595
|
+
declare function createServerClient(secretKey: string, options?: Omit<SPAPSConfig, 'publishableKey' | 'secretKey' | 'apiKey'>): SPAPSClient;
|
|
596
|
+
/**
|
|
597
|
+
* Detect key type from key prefix
|
|
598
|
+
*/
|
|
599
|
+
declare function detectKeyType(key: string): ApiKeyType | null;
|
|
449
600
|
|
|
450
|
-
export { type AdminConfig, DEFAULT_ADMIN_ACCOUNTS, type PermissionCheckResult, PermissionChecker, SPAPSClient as SPAPS, SPAPSClient, type SPAPSConfig, TokenManager, WalletUtils, canAccessAdmin, createPermissionChecker, SPAPSClient as default, defaultPermissionChecker, getRoleAwareErrorMessage, getUserDisplay, getUserRole, hasPermission, isAdminAccount, verifyCryptoWebhookSignature };
|
|
601
|
+
export { type AdminConfig, type ApiKeyType, type CheckoutLineItem, type CheckoutLineItemPriceData, type CreateCheckoutSessionPayload, DEFAULT_ADMIN_ACCOUNTS, type PermissionCheckResult, PermissionChecker, SPAPSClient as SPAPS, SPAPSClient, type SPAPSConfig, TokenManager, WalletUtils, canAccessAdmin, createBrowserClient, createPermissionChecker, createServerClient, SPAPSClient as default, defaultPermissionChecker, detectKeyType, getRoleAwareErrorMessage, getUserDisplay, getUserRole, hasPermission, isAdminAccount, verifyCryptoWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -201,10 +201,13 @@ __export(index_exports, {
|
|
|
201
201
|
TokenManager: () => TokenManager,
|
|
202
202
|
WalletUtils: () => WalletUtils,
|
|
203
203
|
canAccessAdmin: () => canAccessAdmin,
|
|
204
|
+
createBrowserClient: () => createBrowserClient,
|
|
204
205
|
createPermissionChecker: () => createPermissionChecker,
|
|
205
206
|
createSecureMessageRequestSchema: () => import_spaps_types.createSecureMessageRequestSchema,
|
|
207
|
+
createServerClient: () => createServerClient,
|
|
206
208
|
default: () => index_default,
|
|
207
209
|
defaultPermissionChecker: () => defaultPermissionChecker,
|
|
210
|
+
detectKeyType: () => detectKeyType,
|
|
208
211
|
getRoleAwareErrorMessage: () => getRoleAwareErrorMessage,
|
|
209
212
|
getUserDisplay: () => getUserDisplay,
|
|
210
213
|
getUserRole: () => getUserRole,
|
|
@@ -279,6 +282,20 @@ var SPAPSClient = class {
|
|
|
279
282
|
};
|
|
280
283
|
constructor(config = {}) {
|
|
281
284
|
const apiUrl = config.apiUrl || process.env.SPAPS_API_URL || process.env.NEXT_PUBLIC_SPAPS_API_URL;
|
|
285
|
+
const isBrowser = typeof window !== "undefined";
|
|
286
|
+
let effectiveApiKey;
|
|
287
|
+
if (config.publishableKey) {
|
|
288
|
+
effectiveApiKey = config.publishableKey;
|
|
289
|
+
} else if (config.secretKey) {
|
|
290
|
+
effectiveApiKey = config.secretKey;
|
|
291
|
+
if (isBrowser) {
|
|
292
|
+
console.warn("\u26A0\uFE0F SPAPS: Using secretKey in browser is not recommended. Use publishableKey instead.");
|
|
293
|
+
}
|
|
294
|
+
} else if (config.apiKey) {
|
|
295
|
+
effectiveApiKey = config.apiKey;
|
|
296
|
+
} else {
|
|
297
|
+
effectiveApiKey = process.env.SPAPS_API_KEY || process.env.NEXT_PUBLIC_SPAPS_API_KEY;
|
|
298
|
+
}
|
|
282
299
|
if (!apiUrl || apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1")) {
|
|
283
300
|
this._isLocalMode = true;
|
|
284
301
|
this.client = import_axios.default.create({
|
|
@@ -289,10 +306,10 @@ var SPAPSClient = class {
|
|
|
289
306
|
}
|
|
290
307
|
});
|
|
291
308
|
} else {
|
|
292
|
-
if (!
|
|
309
|
+
if (!effectiveApiKey) {
|
|
293
310
|
console.warn("\u26A0\uFE0F SPAPS: No API key provided. Some features may not work.");
|
|
294
311
|
}
|
|
295
|
-
this.apiKey =
|
|
312
|
+
this.apiKey = effectiveApiKey;
|
|
296
313
|
this.client = import_axios.default.create({
|
|
297
314
|
baseURL: apiUrl,
|
|
298
315
|
timeout: config.timeout || 1e4,
|
|
@@ -453,12 +470,24 @@ var SPAPSClient = class {
|
|
|
453
470
|
return data;
|
|
454
471
|
},
|
|
455
472
|
/**
|
|
456
|
-
* Verify a magic link token
|
|
457
|
-
*
|
|
473
|
+
* Verify a magic link token and authenticate the user.
|
|
474
|
+
* Returns full auth response with tokens and auto-stores them.
|
|
458
475
|
*/
|
|
459
476
|
verifyMagicLink: async (payload) => {
|
|
460
|
-
const res = await this.client.post("/api/auth/verify-magic-link",
|
|
461
|
-
|
|
477
|
+
const res = await this.client.post("/api/auth/verify-magic-link", {
|
|
478
|
+
token: payload.token,
|
|
479
|
+
type: payload.type || "magiclink"
|
|
480
|
+
});
|
|
481
|
+
const body = res.data;
|
|
482
|
+
if (body?.success === false) throw new Error(body?.error?.message || "Magic link verification failed");
|
|
483
|
+
const data = {
|
|
484
|
+
access_token: body.tokens?.access_token || body.access_token,
|
|
485
|
+
refresh_token: body.tokens?.refresh_token || body.refresh_token,
|
|
486
|
+
user: body.user
|
|
487
|
+
};
|
|
488
|
+
this.accessToken = data.access_token;
|
|
489
|
+
this.refreshToken = data.refresh_token;
|
|
490
|
+
return data;
|
|
462
491
|
},
|
|
463
492
|
solana: {
|
|
464
493
|
linkWallet: async (payload) => {
|
|
@@ -600,11 +629,25 @@ var SPAPSClient = class {
|
|
|
600
629
|
return this.unwrapApiResponse(res, "Failed to create checkout session");
|
|
601
630
|
},
|
|
602
631
|
createPaymentCheckout: async (params) => {
|
|
603
|
-
const payload = {
|
|
632
|
+
const payload = {
|
|
633
|
+
mode: "payment",
|
|
634
|
+
line_items: [{ price_id: params.price_id, quantity: params.quantity ?? 1 }],
|
|
635
|
+
success_url: params.success_url,
|
|
636
|
+
cancel_url: params.cancel_url,
|
|
637
|
+
require_legal_consent: params.require_legal_consent,
|
|
638
|
+
legal_consent_text: params.legal_consent_text
|
|
639
|
+
};
|
|
604
640
|
return this.payments.createCheckoutSession(payload);
|
|
605
641
|
},
|
|
606
642
|
createSubscriptionCheckout: async (params) => {
|
|
607
|
-
const payload = {
|
|
643
|
+
const payload = {
|
|
644
|
+
mode: "subscription",
|
|
645
|
+
line_items: [{ price_id: params.price_id, quantity: 1 }],
|
|
646
|
+
success_url: params.success_url,
|
|
647
|
+
cancel_url: params.cancel_url,
|
|
648
|
+
require_legal_consent: params.require_legal_consent,
|
|
649
|
+
legal_consent_text: params.legal_consent_text
|
|
650
|
+
};
|
|
608
651
|
if (params.trial_period_days) payload.subscription_data = { trial_period_days: params.trial_period_days };
|
|
609
652
|
return this.payments.createCheckoutSession(payload);
|
|
610
653
|
},
|
|
@@ -760,6 +803,40 @@ var SPAPSClient = class {
|
|
|
760
803
|
return this.unwrapApiResponse(res, "Failed to touch session");
|
|
761
804
|
}
|
|
762
805
|
};
|
|
806
|
+
/**
|
|
807
|
+
* CFO (QuickBooks) namespace for managing multi-account connections
|
|
808
|
+
*/
|
|
809
|
+
cfo = {
|
|
810
|
+
/**
|
|
811
|
+
* Get all QuickBooks connections for the current user
|
|
812
|
+
*/
|
|
813
|
+
getConnections: async () => {
|
|
814
|
+
const res = await this.client.get("/api/cfo/user-connections", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
815
|
+
return this.unwrapApiResponse(res, "Failed to get connections");
|
|
816
|
+
},
|
|
817
|
+
/**
|
|
818
|
+
* Get connection status (returns primary connection for backward compatibility)
|
|
819
|
+
*/
|
|
820
|
+
getStatus: async () => {
|
|
821
|
+
const res = await this.client.get("/api/cfo/user-status", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
822
|
+
return this.unwrapApiResponse(res, "Failed to get status");
|
|
823
|
+
},
|
|
824
|
+
/**
|
|
825
|
+
* Start adding a new QuickBooks connection
|
|
826
|
+
* Returns auth URL to redirect user to QuickBooks OAuth
|
|
827
|
+
*/
|
|
828
|
+
addConnection: async () => {
|
|
829
|
+
const res = await this.client.post("/api/cfo/user-add-connection", {}, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
830
|
+
return this.unwrapApiResponse(res, "Failed to start connection");
|
|
831
|
+
},
|
|
832
|
+
/**
|
|
833
|
+
* Disconnect a specific QuickBooks account
|
|
834
|
+
*/
|
|
835
|
+
disconnect: async (connectionId) => {
|
|
836
|
+
const res = await this.client.delete(`/api/cfo/user-connections/${connectionId}`, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
837
|
+
return this.unwrapApiResponse(res, "Failed to disconnect");
|
|
838
|
+
}
|
|
839
|
+
};
|
|
763
840
|
// Stripe Methods
|
|
764
841
|
async createCheckoutSession(priceId, successUrl, cancelUrl) {
|
|
765
842
|
return this.client.post("/api/stripe/create-checkout-session", {
|
|
@@ -883,6 +960,19 @@ var SPAPSClient = class {
|
|
|
883
960
|
setAccessToken(token) {
|
|
884
961
|
this.accessToken = token;
|
|
885
962
|
}
|
|
963
|
+
setRefreshToken(token) {
|
|
964
|
+
this.refreshToken = token;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Restore tokens (for SSO/OAuth flows)
|
|
968
|
+
* Sets both access and refresh tokens
|
|
969
|
+
*/
|
|
970
|
+
restoreTokens(accessToken, refreshToken) {
|
|
971
|
+
this.accessToken = accessToken;
|
|
972
|
+
if (refreshToken) {
|
|
973
|
+
this.refreshToken = refreshToken;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
886
976
|
isLocalMode() {
|
|
887
977
|
return this._isLocalMode;
|
|
888
978
|
}
|
|
@@ -937,6 +1027,17 @@ var TokenManager = class _TokenManager {
|
|
|
937
1027
|
static ACCESS_TOKEN_KEY = "sweet_potato_access_token";
|
|
938
1028
|
static REFRESH_TOKEN_KEY = "sweet_potato_refresh_token";
|
|
939
1029
|
static USER_KEY = "sweet_potato_user";
|
|
1030
|
+
/**
|
|
1031
|
+
* Cross-platform base64 decode (works in both Node.js and browser)
|
|
1032
|
+
*/
|
|
1033
|
+
static base64Decode(str) {
|
|
1034
|
+
if (typeof Buffer !== "undefined") {
|
|
1035
|
+
return Buffer.from(str, "base64").toString("utf8");
|
|
1036
|
+
} else if (typeof atob !== "undefined") {
|
|
1037
|
+
return atob(str);
|
|
1038
|
+
}
|
|
1039
|
+
throw new Error("No base64 decode method available");
|
|
1040
|
+
}
|
|
940
1041
|
static getStorage() {
|
|
941
1042
|
if (typeof globalThis !== "undefined" && globalThis.localStorage) return globalThis.localStorage;
|
|
942
1043
|
if (typeof globalThis !== "undefined" && globalThis.window?.localStorage) return globalThis.window.localStorage;
|
|
@@ -977,7 +1078,7 @@ var TokenManager = class _TokenManager {
|
|
|
977
1078
|
try {
|
|
978
1079
|
const parts = token.split(".");
|
|
979
1080
|
if (parts.length !== 3 || !parts[1]) return true;
|
|
980
|
-
const payload = JSON.parse(
|
|
1081
|
+
const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
|
|
981
1082
|
const now = Math.floor(Date.now() / 1e3);
|
|
982
1083
|
return payload.exp < now;
|
|
983
1084
|
} catch {
|
|
@@ -1001,6 +1102,74 @@ var TokenManager = class _TokenManager {
|
|
|
1001
1102
|
return false;
|
|
1002
1103
|
}
|
|
1003
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Parse auth tokens from URL fragment (for OAuth callbacks)
|
|
1107
|
+
* URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
|
|
1108
|
+
*/
|
|
1109
|
+
static parseTokensFromUrlFragment(url) {
|
|
1110
|
+
let hash;
|
|
1111
|
+
if (url) {
|
|
1112
|
+
try {
|
|
1113
|
+
hash = new URL(url).hash;
|
|
1114
|
+
} catch {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
} else if (typeof window !== "undefined") {
|
|
1118
|
+
hash = window.location.hash;
|
|
1119
|
+
} else {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
if (!hash || hash.length < 2) return null;
|
|
1123
|
+
const params = new URLSearchParams(hash.substring(1));
|
|
1124
|
+
const access_token = params.get("access_token");
|
|
1125
|
+
const refresh_token = params.get("refresh_token");
|
|
1126
|
+
if (!access_token) return null;
|
|
1127
|
+
return {
|
|
1128
|
+
access_token,
|
|
1129
|
+
refresh_token: refresh_token || void 0,
|
|
1130
|
+
user_id: params.get("user_id") || void 0
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Handle OAuth callback - parse tokens from URL and store them
|
|
1135
|
+
* Returns user info decoded from JWT, or null if no tokens found
|
|
1136
|
+
*/
|
|
1137
|
+
static handleOAuthCallback(sdk) {
|
|
1138
|
+
const tokens = _TokenManager.parseTokensFromUrlFragment();
|
|
1139
|
+
if (!tokens?.access_token) {
|
|
1140
|
+
console.debug("[SPAPS] handleOAuthCallback: No access token found in URL fragment");
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
if (typeof window !== "undefined") {
|
|
1144
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
const parts = tokens.access_token.split(".");
|
|
1148
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
1149
|
+
console.warn("[SPAPS] handleOAuthCallback: Invalid JWT format - expected 3 parts");
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
|
|
1153
|
+
const user = {
|
|
1154
|
+
id: payload.user_id || payload.sub,
|
|
1155
|
+
email: payload.email,
|
|
1156
|
+
role: payload.role || "user"
|
|
1157
|
+
};
|
|
1158
|
+
_TokenManager.storeTokens({
|
|
1159
|
+
access_token: tokens.access_token,
|
|
1160
|
+
refresh_token: tokens.refresh_token || "",
|
|
1161
|
+
user
|
|
1162
|
+
});
|
|
1163
|
+
sdk.setAccessToken(tokens.access_token);
|
|
1164
|
+
if (tokens.refresh_token) {
|
|
1165
|
+
sdk.setRefreshToken(tokens.refresh_token);
|
|
1166
|
+
}
|
|
1167
|
+
return user;
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
console.warn("[SPAPS] handleOAuthCallback: Failed to decode JWT", err);
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1004
1173
|
};
|
|
1005
1174
|
var WalletUtils = class _WalletUtils {
|
|
1006
1175
|
static detectChainType(address) {
|
|
@@ -1026,6 +1195,29 @@ var WalletUtils = class _WalletUtils {
|
|
|
1026
1195
|
}
|
|
1027
1196
|
}
|
|
1028
1197
|
};
|
|
1198
|
+
function createBrowserClient(publishableKey, options) {
|
|
1199
|
+
if (!publishableKey.startsWith("spaps_pub_")) {
|
|
1200
|
+
console.warn("\u26A0\uFE0F SPAPS: Expected a publishable key (spaps_pub_xxx). Using a secret key in browser is not recommended.");
|
|
1201
|
+
}
|
|
1202
|
+
return new SPAPSClient({
|
|
1203
|
+
...options,
|
|
1204
|
+
publishableKey
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
function createServerClient(secretKey, options) {
|
|
1208
|
+
if (typeof window !== "undefined") {
|
|
1209
|
+
console.warn("\u26A0\uFE0F SPAPS: createServerClient should only be used in server environments. Use createBrowserClient for browser usage.");
|
|
1210
|
+
}
|
|
1211
|
+
return new SPAPSClient({
|
|
1212
|
+
...options,
|
|
1213
|
+
secretKey
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function detectKeyType(key) {
|
|
1217
|
+
if (key.startsWith("spaps_pub_")) return "publishable";
|
|
1218
|
+
if (key.startsWith("spaps_sec_") || key.startsWith("spaps_")) return "secret";
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1029
1221
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1030
1222
|
0 && (module.exports = {
|
|
1031
1223
|
DEFAULT_ADMIN_ACCOUNTS,
|
|
@@ -1035,9 +1227,12 @@ var WalletUtils = class _WalletUtils {
|
|
|
1035
1227
|
TokenManager,
|
|
1036
1228
|
WalletUtils,
|
|
1037
1229
|
canAccessAdmin,
|
|
1230
|
+
createBrowserClient,
|
|
1038
1231
|
createPermissionChecker,
|
|
1039
1232
|
createSecureMessageRequestSchema,
|
|
1233
|
+
createServerClient,
|
|
1040
1234
|
defaultPermissionChecker,
|
|
1235
|
+
detectKeyType,
|
|
1041
1236
|
getRoleAwareErrorMessage,
|
|
1042
1237
|
getUserDisplay,
|
|
1043
1238
|
getUserRole,
|
package/dist/index.mjs
CHANGED
|
@@ -255,6 +255,20 @@ var SPAPSClient = class {
|
|
|
255
255
|
};
|
|
256
256
|
constructor(config = {}) {
|
|
257
257
|
const apiUrl = config.apiUrl || process.env.SPAPS_API_URL || process.env.NEXT_PUBLIC_SPAPS_API_URL;
|
|
258
|
+
const isBrowser = typeof window !== "undefined";
|
|
259
|
+
let effectiveApiKey;
|
|
260
|
+
if (config.publishableKey) {
|
|
261
|
+
effectiveApiKey = config.publishableKey;
|
|
262
|
+
} else if (config.secretKey) {
|
|
263
|
+
effectiveApiKey = config.secretKey;
|
|
264
|
+
if (isBrowser) {
|
|
265
|
+
console.warn("\u26A0\uFE0F SPAPS: Using secretKey in browser is not recommended. Use publishableKey instead.");
|
|
266
|
+
}
|
|
267
|
+
} else if (config.apiKey) {
|
|
268
|
+
effectiveApiKey = config.apiKey;
|
|
269
|
+
} else {
|
|
270
|
+
effectiveApiKey = process.env.SPAPS_API_KEY || process.env.NEXT_PUBLIC_SPAPS_API_KEY;
|
|
271
|
+
}
|
|
258
272
|
if (!apiUrl || apiUrl.includes("localhost") || apiUrl.includes("127.0.0.1")) {
|
|
259
273
|
this._isLocalMode = true;
|
|
260
274
|
this.client = axios.create({
|
|
@@ -265,10 +279,10 @@ var SPAPSClient = class {
|
|
|
265
279
|
}
|
|
266
280
|
});
|
|
267
281
|
} else {
|
|
268
|
-
if (!
|
|
282
|
+
if (!effectiveApiKey) {
|
|
269
283
|
console.warn("\u26A0\uFE0F SPAPS: No API key provided. Some features may not work.");
|
|
270
284
|
}
|
|
271
|
-
this.apiKey =
|
|
285
|
+
this.apiKey = effectiveApiKey;
|
|
272
286
|
this.client = axios.create({
|
|
273
287
|
baseURL: apiUrl,
|
|
274
288
|
timeout: config.timeout || 1e4,
|
|
@@ -429,12 +443,24 @@ var SPAPSClient = class {
|
|
|
429
443
|
return data;
|
|
430
444
|
},
|
|
431
445
|
/**
|
|
432
|
-
* Verify a magic link token
|
|
433
|
-
*
|
|
446
|
+
* Verify a magic link token and authenticate the user.
|
|
447
|
+
* Returns full auth response with tokens and auto-stores them.
|
|
434
448
|
*/
|
|
435
449
|
verifyMagicLink: async (payload) => {
|
|
436
|
-
const res = await this.client.post("/api/auth/verify-magic-link",
|
|
437
|
-
|
|
450
|
+
const res = await this.client.post("/api/auth/verify-magic-link", {
|
|
451
|
+
token: payload.token,
|
|
452
|
+
type: payload.type || "magiclink"
|
|
453
|
+
});
|
|
454
|
+
const body = res.data;
|
|
455
|
+
if (body?.success === false) throw new Error(body?.error?.message || "Magic link verification failed");
|
|
456
|
+
const data = {
|
|
457
|
+
access_token: body.tokens?.access_token || body.access_token,
|
|
458
|
+
refresh_token: body.tokens?.refresh_token || body.refresh_token,
|
|
459
|
+
user: body.user
|
|
460
|
+
};
|
|
461
|
+
this.accessToken = data.access_token;
|
|
462
|
+
this.refreshToken = data.refresh_token;
|
|
463
|
+
return data;
|
|
438
464
|
},
|
|
439
465
|
solana: {
|
|
440
466
|
linkWallet: async (payload) => {
|
|
@@ -576,11 +602,25 @@ var SPAPSClient = class {
|
|
|
576
602
|
return this.unwrapApiResponse(res, "Failed to create checkout session");
|
|
577
603
|
},
|
|
578
604
|
createPaymentCheckout: async (params) => {
|
|
579
|
-
const payload = {
|
|
605
|
+
const payload = {
|
|
606
|
+
mode: "payment",
|
|
607
|
+
line_items: [{ price_id: params.price_id, quantity: params.quantity ?? 1 }],
|
|
608
|
+
success_url: params.success_url,
|
|
609
|
+
cancel_url: params.cancel_url,
|
|
610
|
+
require_legal_consent: params.require_legal_consent,
|
|
611
|
+
legal_consent_text: params.legal_consent_text
|
|
612
|
+
};
|
|
580
613
|
return this.payments.createCheckoutSession(payload);
|
|
581
614
|
},
|
|
582
615
|
createSubscriptionCheckout: async (params) => {
|
|
583
|
-
const payload = {
|
|
616
|
+
const payload = {
|
|
617
|
+
mode: "subscription",
|
|
618
|
+
line_items: [{ price_id: params.price_id, quantity: 1 }],
|
|
619
|
+
success_url: params.success_url,
|
|
620
|
+
cancel_url: params.cancel_url,
|
|
621
|
+
require_legal_consent: params.require_legal_consent,
|
|
622
|
+
legal_consent_text: params.legal_consent_text
|
|
623
|
+
};
|
|
584
624
|
if (params.trial_period_days) payload.subscription_data = { trial_period_days: params.trial_period_days };
|
|
585
625
|
return this.payments.createCheckoutSession(payload);
|
|
586
626
|
},
|
|
@@ -736,6 +776,40 @@ var SPAPSClient = class {
|
|
|
736
776
|
return this.unwrapApiResponse(res, "Failed to touch session");
|
|
737
777
|
}
|
|
738
778
|
};
|
|
779
|
+
/**
|
|
780
|
+
* CFO (QuickBooks) namespace for managing multi-account connections
|
|
781
|
+
*/
|
|
782
|
+
cfo = {
|
|
783
|
+
/**
|
|
784
|
+
* Get all QuickBooks connections for the current user
|
|
785
|
+
*/
|
|
786
|
+
getConnections: async () => {
|
|
787
|
+
const res = await this.client.get("/api/cfo/user-connections", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
788
|
+
return this.unwrapApiResponse(res, "Failed to get connections");
|
|
789
|
+
},
|
|
790
|
+
/**
|
|
791
|
+
* Get connection status (returns primary connection for backward compatibility)
|
|
792
|
+
*/
|
|
793
|
+
getStatus: async () => {
|
|
794
|
+
const res = await this.client.get("/api/cfo/user-status", this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
795
|
+
return this.unwrapApiResponse(res, "Failed to get status");
|
|
796
|
+
},
|
|
797
|
+
/**
|
|
798
|
+
* Start adding a new QuickBooks connection
|
|
799
|
+
* Returns auth URL to redirect user to QuickBooks OAuth
|
|
800
|
+
*/
|
|
801
|
+
addConnection: async () => {
|
|
802
|
+
const res = await this.client.post("/api/cfo/user-add-connection", {}, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
803
|
+
return this.unwrapApiResponse(res, "Failed to start connection");
|
|
804
|
+
},
|
|
805
|
+
/**
|
|
806
|
+
* Disconnect a specific QuickBooks account
|
|
807
|
+
*/
|
|
808
|
+
disconnect: async (connectionId) => {
|
|
809
|
+
const res = await this.client.delete(`/api/cfo/user-connections/${connectionId}`, this.accessToken ? { headers: { Authorization: `Bearer ${this.accessToken}` } } : void 0);
|
|
810
|
+
return this.unwrapApiResponse(res, "Failed to disconnect");
|
|
811
|
+
}
|
|
812
|
+
};
|
|
739
813
|
// Stripe Methods
|
|
740
814
|
async createCheckoutSession(priceId, successUrl, cancelUrl) {
|
|
741
815
|
return this.client.post("/api/stripe/create-checkout-session", {
|
|
@@ -859,6 +933,19 @@ var SPAPSClient = class {
|
|
|
859
933
|
setAccessToken(token) {
|
|
860
934
|
this.accessToken = token;
|
|
861
935
|
}
|
|
936
|
+
setRefreshToken(token) {
|
|
937
|
+
this.refreshToken = token;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Restore tokens (for SSO/OAuth flows)
|
|
941
|
+
* Sets both access and refresh tokens
|
|
942
|
+
*/
|
|
943
|
+
restoreTokens(accessToken, refreshToken) {
|
|
944
|
+
this.accessToken = accessToken;
|
|
945
|
+
if (refreshToken) {
|
|
946
|
+
this.refreshToken = refreshToken;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
862
949
|
isLocalMode() {
|
|
863
950
|
return this._isLocalMode;
|
|
864
951
|
}
|
|
@@ -913,6 +1000,17 @@ var TokenManager = class _TokenManager {
|
|
|
913
1000
|
static ACCESS_TOKEN_KEY = "sweet_potato_access_token";
|
|
914
1001
|
static REFRESH_TOKEN_KEY = "sweet_potato_refresh_token";
|
|
915
1002
|
static USER_KEY = "sweet_potato_user";
|
|
1003
|
+
/**
|
|
1004
|
+
* Cross-platform base64 decode (works in both Node.js and browser)
|
|
1005
|
+
*/
|
|
1006
|
+
static base64Decode(str) {
|
|
1007
|
+
if (typeof Buffer !== "undefined") {
|
|
1008
|
+
return Buffer.from(str, "base64").toString("utf8");
|
|
1009
|
+
} else if (typeof atob !== "undefined") {
|
|
1010
|
+
return atob(str);
|
|
1011
|
+
}
|
|
1012
|
+
throw new Error("No base64 decode method available");
|
|
1013
|
+
}
|
|
916
1014
|
static getStorage() {
|
|
917
1015
|
if (typeof globalThis !== "undefined" && globalThis.localStorage) return globalThis.localStorage;
|
|
918
1016
|
if (typeof globalThis !== "undefined" && globalThis.window?.localStorage) return globalThis.window.localStorage;
|
|
@@ -953,7 +1051,7 @@ var TokenManager = class _TokenManager {
|
|
|
953
1051
|
try {
|
|
954
1052
|
const parts = token.split(".");
|
|
955
1053
|
if (parts.length !== 3 || !parts[1]) return true;
|
|
956
|
-
const payload = JSON.parse(
|
|
1054
|
+
const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
|
|
957
1055
|
const now = Math.floor(Date.now() / 1e3);
|
|
958
1056
|
return payload.exp < now;
|
|
959
1057
|
} catch {
|
|
@@ -977,6 +1075,74 @@ var TokenManager = class _TokenManager {
|
|
|
977
1075
|
return false;
|
|
978
1076
|
}
|
|
979
1077
|
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Parse auth tokens from URL fragment (for OAuth callbacks)
|
|
1080
|
+
* URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
|
|
1081
|
+
*/
|
|
1082
|
+
static parseTokensFromUrlFragment(url) {
|
|
1083
|
+
let hash;
|
|
1084
|
+
if (url) {
|
|
1085
|
+
try {
|
|
1086
|
+
hash = new URL(url).hash;
|
|
1087
|
+
} catch {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
} else if (typeof window !== "undefined") {
|
|
1091
|
+
hash = window.location.hash;
|
|
1092
|
+
} else {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
if (!hash || hash.length < 2) return null;
|
|
1096
|
+
const params = new URLSearchParams(hash.substring(1));
|
|
1097
|
+
const access_token = params.get("access_token");
|
|
1098
|
+
const refresh_token = params.get("refresh_token");
|
|
1099
|
+
if (!access_token) return null;
|
|
1100
|
+
return {
|
|
1101
|
+
access_token,
|
|
1102
|
+
refresh_token: refresh_token || void 0,
|
|
1103
|
+
user_id: params.get("user_id") || void 0
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Handle OAuth callback - parse tokens from URL and store them
|
|
1108
|
+
* Returns user info decoded from JWT, or null if no tokens found
|
|
1109
|
+
*/
|
|
1110
|
+
static handleOAuthCallback(sdk) {
|
|
1111
|
+
const tokens = _TokenManager.parseTokensFromUrlFragment();
|
|
1112
|
+
if (!tokens?.access_token) {
|
|
1113
|
+
console.debug("[SPAPS] handleOAuthCallback: No access token found in URL fragment");
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
if (typeof window !== "undefined") {
|
|
1117
|
+
window.history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const parts = tokens.access_token.split(".");
|
|
1121
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
1122
|
+
console.warn("[SPAPS] handleOAuthCallback: Invalid JWT format - expected 3 parts");
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
|
|
1126
|
+
const user = {
|
|
1127
|
+
id: payload.user_id || payload.sub,
|
|
1128
|
+
email: payload.email,
|
|
1129
|
+
role: payload.role || "user"
|
|
1130
|
+
};
|
|
1131
|
+
_TokenManager.storeTokens({
|
|
1132
|
+
access_token: tokens.access_token,
|
|
1133
|
+
refresh_token: tokens.refresh_token || "",
|
|
1134
|
+
user
|
|
1135
|
+
});
|
|
1136
|
+
sdk.setAccessToken(tokens.access_token);
|
|
1137
|
+
if (tokens.refresh_token) {
|
|
1138
|
+
sdk.setRefreshToken(tokens.refresh_token);
|
|
1139
|
+
}
|
|
1140
|
+
return user;
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
console.warn("[SPAPS] handleOAuthCallback: Failed to decode JWT", err);
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
980
1146
|
};
|
|
981
1147
|
var WalletUtils = class _WalletUtils {
|
|
982
1148
|
static detectChainType(address) {
|
|
@@ -1002,6 +1168,29 @@ var WalletUtils = class _WalletUtils {
|
|
|
1002
1168
|
}
|
|
1003
1169
|
}
|
|
1004
1170
|
};
|
|
1171
|
+
function createBrowserClient(publishableKey, options) {
|
|
1172
|
+
if (!publishableKey.startsWith("spaps_pub_")) {
|
|
1173
|
+
console.warn("\u26A0\uFE0F SPAPS: Expected a publishable key (spaps_pub_xxx). Using a secret key in browser is not recommended.");
|
|
1174
|
+
}
|
|
1175
|
+
return new SPAPSClient({
|
|
1176
|
+
...options,
|
|
1177
|
+
publishableKey
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
function createServerClient(secretKey, options) {
|
|
1181
|
+
if (typeof window !== "undefined") {
|
|
1182
|
+
console.warn("\u26A0\uFE0F SPAPS: createServerClient should only be used in server environments. Use createBrowserClient for browser usage.");
|
|
1183
|
+
}
|
|
1184
|
+
return new SPAPSClient({
|
|
1185
|
+
...options,
|
|
1186
|
+
secretKey
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
function detectKeyType(key) {
|
|
1190
|
+
if (key.startsWith("spaps_pub_")) return "publishable";
|
|
1191
|
+
if (key.startsWith("spaps_sec_") || key.startsWith("spaps_")) return "secret";
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1005
1194
|
export {
|
|
1006
1195
|
DEFAULT_ADMIN_ACCOUNTS,
|
|
1007
1196
|
PermissionChecker,
|
|
@@ -1010,10 +1199,13 @@ export {
|
|
|
1010
1199
|
TokenManager,
|
|
1011
1200
|
WalletUtils,
|
|
1012
1201
|
canAccessAdmin,
|
|
1202
|
+
createBrowserClient,
|
|
1013
1203
|
createPermissionChecker,
|
|
1014
1204
|
createSecureMessageRequestSchema,
|
|
1205
|
+
createServerClient,
|
|
1015
1206
|
index_default as default,
|
|
1016
1207
|
defaultPermissionChecker,
|
|
1208
|
+
detectKeyType,
|
|
1017
1209
|
getRoleAwareErrorMessage,
|
|
1018
1210
|
getUserDisplay,
|
|
1019
1211
|
getUserRole,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spaps-sdk",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Sweet Potato Authentication & Payment Service SDK - Zero-config client with built-in permission checking and role-based access control",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"email": "buildooor@gmail.com"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"spaps-types": "^1.0.
|
|
47
|
+
"spaps-types": "^1.0.59",
|
|
48
48
|
"axios": "^1.6.0",
|
|
49
49
|
"cross-fetch": "^4.0.0"
|
|
50
50
|
},
|