spaps-sdk 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as spaps_types from 'spaps-types';
2
- import { CreateProductRequest, Product, UpdateProductRequest, CreatePriceRequest, Price, ProductSyncResult, CryptoReconcileRequest, CreateSecureMessageRequest, SecureMessage, AuthResponse, User as User$1, CreateCryptoInvoiceRequest, CryptoInvoiceStatusSnapshot, CheckoutSession, Subscription, UsageBalance, VerifyCryptoWebhookSignatureOptions } from 'spaps-types';
3
- export { AdminPermission, AdminRole, AdminUser, ApiResponse, AuthResponse, CheckoutSession, CreateCryptoInvoiceRequest, CreatePriceRequest, CreateProductRequest, CreateSecureMessageInput, CreateSecureMessageRequest, CryptoInvoice, CryptoInvoiceResponse, CryptoInvoiceStatusSnapshot, CryptoReconcileRequest, Price, Product, ProductSyncResult, SecureMessage, SecureMessageOutput, Subscription, TokenPair, UpdateProductRequest, UsageBalance, User, UserProfile, UserRole, UserWallet, VerifyCryptoWebhookSignatureOptions, createSecureMessageRequestSchema, secureMessageMetadataSchema, secureMessageSchema } from 'spaps-types';
2
+ import { CreateProductRequest, Product, UpdateProductRequest, CreatePriceRequest, Price, ProductSyncResult, CryptoReconcileRequest, CreateSecureMessageRequest, SecureMessage, AuthResponse, User as User$1, CreateCryptoInvoiceRequest, CryptoInvoiceStatusSnapshot, CheckoutSession, DayrateAvailabilityResponse, DayrateBookingRequest, DayrateBookingResponse, DayrateMultiBookingRequest, DayrateMultiBookingResponse, Subscription, UsageBalance, VerifyCryptoWebhookSignatureOptions } from 'spaps-types';
3
+ export { AdminPermission, AdminRole, AdminUser, ApiResponse, AuthResponse, CheckoutSession, CreateCryptoInvoiceRequest, CreatePriceRequest, CreateProductRequest, CreateSecureMessageInput, CreateSecureMessageRequest, CryptoInvoice, CryptoInvoiceResponse, CryptoInvoiceStatusSnapshot, CryptoReconcileRequest, DayrateAvailabilityResponse, DayrateAvailableSlot, DayrateBookingRequest, DayrateBookingResponse, DayrateDayOfWeek, DayrateMultiBookingRequest, DayrateMultiBookingResponse, DayratePriceBreakdown, DayrateSlotType, Price, Product, ProductSyncResult, SecureMessage, SecureMessageOutput, Subscription, TokenPair, UpdateProductRequest, UsageBalance, User, UserProfile, UserRole, UserWallet, VerifyCryptoWebhookSignatureOptions, createSecureMessageRequestSchema, secureMessageMetadataSchema, secureMessageSchema } from 'spaps-types';
4
4
 
5
5
  /**
6
6
  * Permission checking utilities for SPAPS SDK
@@ -244,14 +244,13 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
244
244
  password: string;
245
245
  }) => Promise<any>;
246
246
  /**
247
- * Verify a magic link token. Does not set access/refresh tokens.
248
- * Consumers should redirect or prompt login based on the returned status.
247
+ * Verify a magic link token and authenticate the user.
248
+ * Returns full auth response with tokens and auto-stores them.
249
249
  */
250
250
  verifyMagicLink: (payload: {
251
251
  token: string;
252
- }) => Promise<{
253
- success: boolean;
254
- }>;
252
+ type?: string;
253
+ }) => Promise<AuthResponse>;
255
254
  solana: {
256
255
  linkWallet: (payload: {
257
256
  wallet_address: string;
@@ -402,6 +401,72 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
402
401
  revokeAll: () => Promise<any>;
403
402
  touch: () => Promise<any>;
404
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
+ };
449
+ /**
450
+ * DayRate (Dynamic Scheduling) namespace
451
+ * For booking half-day sessions with dynamic pricing
452
+ */
453
+ dayrate: {
454
+ /**
455
+ * Get available slots with current pricing
456
+ * Public endpoint - no authentication required
457
+ */
458
+ getAvailability: () => Promise<DayrateAvailabilityResponse>;
459
+ /**
460
+ * Create a single-slot booking and get Stripe checkout URL
461
+ * Returns a 10-minute reservation with checkout link
462
+ */
463
+ createBooking: (payload: DayrateBookingRequest) => Promise<DayrateBookingResponse>;
464
+ /**
465
+ * Create a multi-slot booking and get Stripe checkout URL
466
+ * Reserves multiple slots with a single checkout session
467
+ */
468
+ createMultiBooking: (payload: DayrateMultiBookingRequest) => Promise<DayrateMultiBookingResponse>;
469
+ };
405
470
  createCheckoutSession(priceId: string, successUrl: string, cancelUrl?: string): Promise<{
406
471
  data: CheckoutSession;
407
472
  }>;
@@ -462,6 +527,12 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
462
527
  isAuthenticated(): boolean;
463
528
  getAccessToken(): string | undefined;
464
529
  setAccessToken(token: string): void;
530
+ setRefreshToken(token: string): void;
531
+ /**
532
+ * Restore tokens (for SSO/OAuth flows)
533
+ * Sets both access and refresh tokens
534
+ */
535
+ restoreTokens(accessToken: string, refreshToken?: string): void;
465
536
  isLocalMode(): boolean;
466
537
  /**
467
538
  * Check if current user has admin privileges
@@ -478,6 +549,10 @@ declare class TokenManager {
478
549
  private static readonly ACCESS_TOKEN_KEY;
479
550
  private static readonly REFRESH_TOKEN_KEY;
480
551
  private static readonly USER_KEY;
552
+ /**
553
+ * Cross-platform base64 decode (works in both Node.js and browser)
554
+ */
555
+ private static base64Decode;
481
556
  private static getStorage;
482
557
  static storeTokens(tokens: AuthResponse): void;
483
558
  static getAccessToken(): string | null;
@@ -486,6 +561,24 @@ declare class TokenManager {
486
561
  static clearTokens(): void;
487
562
  static isTokenExpired(token: string): boolean;
488
563
  static autoRefreshToken(sdk: SPAPSClient): Promise<boolean>;
564
+ /**
565
+ * Parse auth tokens from URL fragment (for OAuth callbacks)
566
+ * URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
567
+ */
568
+ static parseTokensFromUrlFragment(url?: string): {
569
+ access_token?: string;
570
+ refresh_token?: string;
571
+ user_id?: string;
572
+ } | null;
573
+ /**
574
+ * Handle OAuth callback - parse tokens from URL and store them
575
+ * Returns user info decoded from JWT, or null if no tokens found
576
+ */
577
+ static handleOAuthCallback(sdk: SPAPSClient): {
578
+ id: string;
579
+ email?: string;
580
+ role?: string;
581
+ } | null;
489
582
  }
490
583
  declare class WalletUtils {
491
584
  static detectChainType(address: string): 'solana' | 'ethereum' | 'bitcoin' | null;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as spaps_types from 'spaps-types';
2
- import { CreateProductRequest, Product, UpdateProductRequest, CreatePriceRequest, Price, ProductSyncResult, CryptoReconcileRequest, CreateSecureMessageRequest, SecureMessage, AuthResponse, User as User$1, CreateCryptoInvoiceRequest, CryptoInvoiceStatusSnapshot, CheckoutSession, Subscription, UsageBalance, VerifyCryptoWebhookSignatureOptions } from 'spaps-types';
3
- export { AdminPermission, AdminRole, AdminUser, ApiResponse, AuthResponse, CheckoutSession, CreateCryptoInvoiceRequest, CreatePriceRequest, CreateProductRequest, CreateSecureMessageInput, CreateSecureMessageRequest, CryptoInvoice, CryptoInvoiceResponse, CryptoInvoiceStatusSnapshot, CryptoReconcileRequest, Price, Product, ProductSyncResult, SecureMessage, SecureMessageOutput, Subscription, TokenPair, UpdateProductRequest, UsageBalance, User, UserProfile, UserRole, UserWallet, VerifyCryptoWebhookSignatureOptions, createSecureMessageRequestSchema, secureMessageMetadataSchema, secureMessageSchema } from 'spaps-types';
2
+ import { CreateProductRequest, Product, UpdateProductRequest, CreatePriceRequest, Price, ProductSyncResult, CryptoReconcileRequest, CreateSecureMessageRequest, SecureMessage, AuthResponse, User as User$1, CreateCryptoInvoiceRequest, CryptoInvoiceStatusSnapshot, CheckoutSession, DayrateAvailabilityResponse, DayrateBookingRequest, DayrateBookingResponse, DayrateMultiBookingRequest, DayrateMultiBookingResponse, Subscription, UsageBalance, VerifyCryptoWebhookSignatureOptions } from 'spaps-types';
3
+ export { AdminPermission, AdminRole, AdminUser, ApiResponse, AuthResponse, CheckoutSession, CreateCryptoInvoiceRequest, CreatePriceRequest, CreateProductRequest, CreateSecureMessageInput, CreateSecureMessageRequest, CryptoInvoice, CryptoInvoiceResponse, CryptoInvoiceStatusSnapshot, CryptoReconcileRequest, DayrateAvailabilityResponse, DayrateAvailableSlot, DayrateBookingRequest, DayrateBookingResponse, DayrateDayOfWeek, DayrateMultiBookingRequest, DayrateMultiBookingResponse, DayratePriceBreakdown, DayrateSlotType, Price, Product, ProductSyncResult, SecureMessage, SecureMessageOutput, Subscription, TokenPair, UpdateProductRequest, UsageBalance, User, UserProfile, UserRole, UserWallet, VerifyCryptoWebhookSignatureOptions, createSecureMessageRequestSchema, secureMessageMetadataSchema, secureMessageSchema } from 'spaps-types';
4
4
 
5
5
  /**
6
6
  * Permission checking utilities for SPAPS SDK
@@ -244,14 +244,13 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
244
244
  password: string;
245
245
  }) => Promise<any>;
246
246
  /**
247
- * Verify a magic link token. Does not set access/refresh tokens.
248
- * Consumers should redirect or prompt login based on the returned status.
247
+ * Verify a magic link token and authenticate the user.
248
+ * Returns full auth response with tokens and auto-stores them.
249
249
  */
250
250
  verifyMagicLink: (payload: {
251
251
  token: string;
252
- }) => Promise<{
253
- success: boolean;
254
- }>;
252
+ type?: string;
253
+ }) => Promise<AuthResponse>;
255
254
  solana: {
256
255
  linkWallet: (payload: {
257
256
  wallet_address: string;
@@ -402,6 +401,72 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
402
401
  revokeAll: () => Promise<any>;
403
402
  touch: () => Promise<any>;
404
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
+ };
449
+ /**
450
+ * DayRate (Dynamic Scheduling) namespace
451
+ * For booking half-day sessions with dynamic pricing
452
+ */
453
+ dayrate: {
454
+ /**
455
+ * Get available slots with current pricing
456
+ * Public endpoint - no authentication required
457
+ */
458
+ getAvailability: () => Promise<DayrateAvailabilityResponse>;
459
+ /**
460
+ * Create a single-slot booking and get Stripe checkout URL
461
+ * Returns a 10-minute reservation with checkout link
462
+ */
463
+ createBooking: (payload: DayrateBookingRequest) => Promise<DayrateBookingResponse>;
464
+ /**
465
+ * Create a multi-slot booking and get Stripe checkout URL
466
+ * Reserves multiple slots with a single checkout session
467
+ */
468
+ createMultiBooking: (payload: DayrateMultiBookingRequest) => Promise<DayrateMultiBookingResponse>;
469
+ };
405
470
  createCheckoutSession(priceId: string, successUrl: string, cancelUrl?: string): Promise<{
406
471
  data: CheckoutSession;
407
472
  }>;
@@ -462,6 +527,12 @@ declare class SPAPSClient<SecureMessageMetadata extends Record<string, any> = Re
462
527
  isAuthenticated(): boolean;
463
528
  getAccessToken(): string | undefined;
464
529
  setAccessToken(token: string): void;
530
+ setRefreshToken(token: string): void;
531
+ /**
532
+ * Restore tokens (for SSO/OAuth flows)
533
+ * Sets both access and refresh tokens
534
+ */
535
+ restoreTokens(accessToken: string, refreshToken?: string): void;
465
536
  isLocalMode(): boolean;
466
537
  /**
467
538
  * Check if current user has admin privileges
@@ -478,6 +549,10 @@ declare class TokenManager {
478
549
  private static readonly ACCESS_TOKEN_KEY;
479
550
  private static readonly REFRESH_TOKEN_KEY;
480
551
  private static readonly USER_KEY;
552
+ /**
553
+ * Cross-platform base64 decode (works in both Node.js and browser)
554
+ */
555
+ private static base64Decode;
481
556
  private static getStorage;
482
557
  static storeTokens(tokens: AuthResponse): void;
483
558
  static getAccessToken(): string | null;
@@ -486,6 +561,24 @@ declare class TokenManager {
486
561
  static clearTokens(): void;
487
562
  static isTokenExpired(token: string): boolean;
488
563
  static autoRefreshToken(sdk: SPAPSClient): Promise<boolean>;
564
+ /**
565
+ * Parse auth tokens from URL fragment (for OAuth callbacks)
566
+ * URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
567
+ */
568
+ static parseTokensFromUrlFragment(url?: string): {
569
+ access_token?: string;
570
+ refresh_token?: string;
571
+ user_id?: string;
572
+ } | null;
573
+ /**
574
+ * Handle OAuth callback - parse tokens from URL and store them
575
+ * Returns user info decoded from JWT, or null if no tokens found
576
+ */
577
+ static handleOAuthCallback(sdk: SPAPSClient): {
578
+ id: string;
579
+ email?: string;
580
+ role?: string;
581
+ } | null;
489
582
  }
490
583
  declare class WalletUtils {
491
584
  static detectChainType(address: string): 'solana' | 'ethereum' | 'bitcoin' | null;
package/dist/index.js CHANGED
@@ -470,12 +470,24 @@ var SPAPSClient = class {
470
470
  return data;
471
471
  },
472
472
  /**
473
- * Verify a magic link token. Does not set access/refresh tokens.
474
- * Consumers should redirect or prompt login based on the returned status.
473
+ * Verify a magic link token and authenticate the user.
474
+ * Returns full auth response with tokens and auto-stores them.
475
475
  */
476
476
  verifyMagicLink: async (payload) => {
477
- const res = await this.client.post("/api/auth/verify-magic-link", payload);
478
- return this.unwrapApiResponse(res, "Failed to verify magic link");
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;
479
491
  },
480
492
  solana: {
481
493
  linkWallet: async (payload) => {
@@ -791,6 +803,70 @@ var SPAPSClient = class {
791
803
  return this.unwrapApiResponse(res, "Failed to touch session");
792
804
  }
793
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
+ };
840
+ /**
841
+ * DayRate (Dynamic Scheduling) namespace
842
+ * For booking half-day sessions with dynamic pricing
843
+ */
844
+ dayrate = {
845
+ /**
846
+ * Get available slots with current pricing
847
+ * Public endpoint - no authentication required
848
+ */
849
+ getAvailability: async () => {
850
+ const res = await this.client.get("/api/dayrate/availability");
851
+ return this.unwrapApiResponse(res, "Failed to get availability");
852
+ },
853
+ /**
854
+ * Create a single-slot booking and get Stripe checkout URL
855
+ * Returns a 10-minute reservation with checkout link
856
+ */
857
+ createBooking: async (payload) => {
858
+ const res = await this.client.post("/api/dayrate/book", payload);
859
+ return this.unwrapApiResponse(res, "Failed to create booking");
860
+ },
861
+ /**
862
+ * Create a multi-slot booking and get Stripe checkout URL
863
+ * Reserves multiple slots with a single checkout session
864
+ */
865
+ createMultiBooking: async (payload) => {
866
+ const res = await this.client.post("/api/dayrate/book-multi", payload);
867
+ return this.unwrapApiResponse(res, "Failed to create multi-booking");
868
+ }
869
+ };
794
870
  // Stripe Methods
795
871
  async createCheckoutSession(priceId, successUrl, cancelUrl) {
796
872
  return this.client.post("/api/stripe/create-checkout-session", {
@@ -914,6 +990,19 @@ var SPAPSClient = class {
914
990
  setAccessToken(token) {
915
991
  this.accessToken = token;
916
992
  }
993
+ setRefreshToken(token) {
994
+ this.refreshToken = token;
995
+ }
996
+ /**
997
+ * Restore tokens (for SSO/OAuth flows)
998
+ * Sets both access and refresh tokens
999
+ */
1000
+ restoreTokens(accessToken, refreshToken) {
1001
+ this.accessToken = accessToken;
1002
+ if (refreshToken) {
1003
+ this.refreshToken = refreshToken;
1004
+ }
1005
+ }
917
1006
  isLocalMode() {
918
1007
  return this._isLocalMode;
919
1008
  }
@@ -968,6 +1057,17 @@ var TokenManager = class _TokenManager {
968
1057
  static ACCESS_TOKEN_KEY = "sweet_potato_access_token";
969
1058
  static REFRESH_TOKEN_KEY = "sweet_potato_refresh_token";
970
1059
  static USER_KEY = "sweet_potato_user";
1060
+ /**
1061
+ * Cross-platform base64 decode (works in both Node.js and browser)
1062
+ */
1063
+ static base64Decode(str) {
1064
+ if (typeof Buffer !== "undefined") {
1065
+ return Buffer.from(str, "base64").toString("utf8");
1066
+ } else if (typeof atob !== "undefined") {
1067
+ return atob(str);
1068
+ }
1069
+ throw new Error("No base64 decode method available");
1070
+ }
971
1071
  static getStorage() {
972
1072
  if (typeof globalThis !== "undefined" && globalThis.localStorage) return globalThis.localStorage;
973
1073
  if (typeof globalThis !== "undefined" && globalThis.window?.localStorage) return globalThis.window.localStorage;
@@ -1008,7 +1108,7 @@ var TokenManager = class _TokenManager {
1008
1108
  try {
1009
1109
  const parts = token.split(".");
1010
1110
  if (parts.length !== 3 || !parts[1]) return true;
1011
- const payload = JSON.parse(atob(parts[1]));
1111
+ const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
1012
1112
  const now = Math.floor(Date.now() / 1e3);
1013
1113
  return payload.exp < now;
1014
1114
  } catch {
@@ -1032,6 +1132,74 @@ var TokenManager = class _TokenManager {
1032
1132
  return false;
1033
1133
  }
1034
1134
  }
1135
+ /**
1136
+ * Parse auth tokens from URL fragment (for OAuth callbacks)
1137
+ * URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
1138
+ */
1139
+ static parseTokensFromUrlFragment(url) {
1140
+ let hash;
1141
+ if (url) {
1142
+ try {
1143
+ hash = new URL(url).hash;
1144
+ } catch {
1145
+ return null;
1146
+ }
1147
+ } else if (typeof window !== "undefined") {
1148
+ hash = window.location.hash;
1149
+ } else {
1150
+ return null;
1151
+ }
1152
+ if (!hash || hash.length < 2) return null;
1153
+ const params = new URLSearchParams(hash.substring(1));
1154
+ const access_token = params.get("access_token");
1155
+ const refresh_token = params.get("refresh_token");
1156
+ if (!access_token) return null;
1157
+ return {
1158
+ access_token,
1159
+ refresh_token: refresh_token || void 0,
1160
+ user_id: params.get("user_id") || void 0
1161
+ };
1162
+ }
1163
+ /**
1164
+ * Handle OAuth callback - parse tokens from URL and store them
1165
+ * Returns user info decoded from JWT, or null if no tokens found
1166
+ */
1167
+ static handleOAuthCallback(sdk) {
1168
+ const tokens = _TokenManager.parseTokensFromUrlFragment();
1169
+ if (!tokens?.access_token) {
1170
+ console.debug("[SPAPS] handleOAuthCallback: No access token found in URL fragment");
1171
+ return null;
1172
+ }
1173
+ if (typeof window !== "undefined") {
1174
+ window.history.replaceState(null, "", window.location.pathname + window.location.search);
1175
+ }
1176
+ try {
1177
+ const parts = tokens.access_token.split(".");
1178
+ if (parts.length !== 3 || !parts[1]) {
1179
+ console.warn("[SPAPS] handleOAuthCallback: Invalid JWT format - expected 3 parts");
1180
+ return null;
1181
+ }
1182
+ const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
1183
+ const user = {
1184
+ id: payload.user_id || payload.sub,
1185
+ email: payload.email,
1186
+ role: payload.role || "user"
1187
+ };
1188
+ _TokenManager.storeTokens({
1189
+ access_token: tokens.access_token,
1190
+ refresh_token: tokens.refresh_token || "",
1191
+ user
1192
+ });
1193
+ sdk.setAccessToken(tokens.access_token);
1194
+ if (tokens.refresh_token) {
1195
+ sdk.setRefreshToken(tokens.refresh_token);
1196
+ }
1197
+ return user;
1198
+ } catch (err) {
1199
+ console.warn("[SPAPS] handleOAuthCallback: Failed to decode JWT", err);
1200
+ return null;
1201
+ }
1202
+ }
1035
1203
  };
1036
1204
  var WalletUtils = class _WalletUtils {
1037
1205
  static detectChainType(address) {
package/dist/index.mjs CHANGED
@@ -443,12 +443,24 @@ var SPAPSClient = class {
443
443
  return data;
444
444
  },
445
445
  /**
446
- * Verify a magic link token. Does not set access/refresh tokens.
447
- * Consumers should redirect or prompt login based on the returned status.
446
+ * Verify a magic link token and authenticate the user.
447
+ * Returns full auth response with tokens and auto-stores them.
448
448
  */
449
449
  verifyMagicLink: async (payload) => {
450
- const res = await this.client.post("/api/auth/verify-magic-link", payload);
451
- return this.unwrapApiResponse(res, "Failed to verify magic link");
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;
452
464
  },
453
465
  solana: {
454
466
  linkWallet: async (payload) => {
@@ -764,6 +776,70 @@ var SPAPSClient = class {
764
776
  return this.unwrapApiResponse(res, "Failed to touch session");
765
777
  }
766
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
+ };
813
+ /**
814
+ * DayRate (Dynamic Scheduling) namespace
815
+ * For booking half-day sessions with dynamic pricing
816
+ */
817
+ dayrate = {
818
+ /**
819
+ * Get available slots with current pricing
820
+ * Public endpoint - no authentication required
821
+ */
822
+ getAvailability: async () => {
823
+ const res = await this.client.get("/api/dayrate/availability");
824
+ return this.unwrapApiResponse(res, "Failed to get availability");
825
+ },
826
+ /**
827
+ * Create a single-slot booking and get Stripe checkout URL
828
+ * Returns a 10-minute reservation with checkout link
829
+ */
830
+ createBooking: async (payload) => {
831
+ const res = await this.client.post("/api/dayrate/book", payload);
832
+ return this.unwrapApiResponse(res, "Failed to create booking");
833
+ },
834
+ /**
835
+ * Create a multi-slot booking and get Stripe checkout URL
836
+ * Reserves multiple slots with a single checkout session
837
+ */
838
+ createMultiBooking: async (payload) => {
839
+ const res = await this.client.post("/api/dayrate/book-multi", payload);
840
+ return this.unwrapApiResponse(res, "Failed to create multi-booking");
841
+ }
842
+ };
767
843
  // Stripe Methods
768
844
  async createCheckoutSession(priceId, successUrl, cancelUrl) {
769
845
  return this.client.post("/api/stripe/create-checkout-session", {
@@ -887,6 +963,19 @@ var SPAPSClient = class {
887
963
  setAccessToken(token) {
888
964
  this.accessToken = token;
889
965
  }
966
+ setRefreshToken(token) {
967
+ this.refreshToken = token;
968
+ }
969
+ /**
970
+ * Restore tokens (for SSO/OAuth flows)
971
+ * Sets both access and refresh tokens
972
+ */
973
+ restoreTokens(accessToken, refreshToken) {
974
+ this.accessToken = accessToken;
975
+ if (refreshToken) {
976
+ this.refreshToken = refreshToken;
977
+ }
978
+ }
890
979
  isLocalMode() {
891
980
  return this._isLocalMode;
892
981
  }
@@ -941,6 +1030,17 @@ var TokenManager = class _TokenManager {
941
1030
  static ACCESS_TOKEN_KEY = "sweet_potato_access_token";
942
1031
  static REFRESH_TOKEN_KEY = "sweet_potato_refresh_token";
943
1032
  static USER_KEY = "sweet_potato_user";
1033
+ /**
1034
+ * Cross-platform base64 decode (works in both Node.js and browser)
1035
+ */
1036
+ static base64Decode(str) {
1037
+ if (typeof Buffer !== "undefined") {
1038
+ return Buffer.from(str, "base64").toString("utf8");
1039
+ } else if (typeof atob !== "undefined") {
1040
+ return atob(str);
1041
+ }
1042
+ throw new Error("No base64 decode method available");
1043
+ }
944
1044
  static getStorage() {
945
1045
  if (typeof globalThis !== "undefined" && globalThis.localStorage) return globalThis.localStorage;
946
1046
  if (typeof globalThis !== "undefined" && globalThis.window?.localStorage) return globalThis.window.localStorage;
@@ -981,7 +1081,7 @@ var TokenManager = class _TokenManager {
981
1081
  try {
982
1082
  const parts = token.split(".");
983
1083
  if (parts.length !== 3 || !parts[1]) return true;
984
- const payload = JSON.parse(atob(parts[1]));
1084
+ const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
985
1085
  const now = Math.floor(Date.now() / 1e3);
986
1086
  return payload.exp < now;
987
1087
  } catch {
@@ -1005,6 +1105,74 @@ var TokenManager = class _TokenManager {
1005
1105
  return false;
1006
1106
  }
1007
1107
  }
1108
+ /**
1109
+ * Parse auth tokens from URL fragment (for OAuth callbacks)
1110
+ * URL format: https://app.com/callback#access_token=xxx&refresh_token=xxx
1111
+ */
1112
+ static parseTokensFromUrlFragment(url) {
1113
+ let hash;
1114
+ if (url) {
1115
+ try {
1116
+ hash = new URL(url).hash;
1117
+ } catch {
1118
+ return null;
1119
+ }
1120
+ } else if (typeof window !== "undefined") {
1121
+ hash = window.location.hash;
1122
+ } else {
1123
+ return null;
1124
+ }
1125
+ if (!hash || hash.length < 2) return null;
1126
+ const params = new URLSearchParams(hash.substring(1));
1127
+ const access_token = params.get("access_token");
1128
+ const refresh_token = params.get("refresh_token");
1129
+ if (!access_token) return null;
1130
+ return {
1131
+ access_token,
1132
+ refresh_token: refresh_token || void 0,
1133
+ user_id: params.get("user_id") || void 0
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Handle OAuth callback - parse tokens from URL and store them
1138
+ * Returns user info decoded from JWT, or null if no tokens found
1139
+ */
1140
+ static handleOAuthCallback(sdk) {
1141
+ const tokens = _TokenManager.parseTokensFromUrlFragment();
1142
+ if (!tokens?.access_token) {
1143
+ console.debug("[SPAPS] handleOAuthCallback: No access token found in URL fragment");
1144
+ return null;
1145
+ }
1146
+ if (typeof window !== "undefined") {
1147
+ window.history.replaceState(null, "", window.location.pathname + window.location.search);
1148
+ }
1149
+ try {
1150
+ const parts = tokens.access_token.split(".");
1151
+ if (parts.length !== 3 || !parts[1]) {
1152
+ console.warn("[SPAPS] handleOAuthCallback: Invalid JWT format - expected 3 parts");
1153
+ return null;
1154
+ }
1155
+ const payload = JSON.parse(_TokenManager.base64Decode(parts[1]));
1156
+ const user = {
1157
+ id: payload.user_id || payload.sub,
1158
+ email: payload.email,
1159
+ role: payload.role || "user"
1160
+ };
1161
+ _TokenManager.storeTokens({
1162
+ access_token: tokens.access_token,
1163
+ refresh_token: tokens.refresh_token || "",
1164
+ user
1165
+ });
1166
+ sdk.setAccessToken(tokens.access_token);
1167
+ if (tokens.refresh_token) {
1168
+ sdk.setRefreshToken(tokens.refresh_token);
1169
+ }
1170
+ return user;
1171
+ } catch (err) {
1172
+ console.warn("[SPAPS] handleOAuthCallback: Failed to decode JWT", err);
1173
+ return null;
1174
+ }
1175
+ }
1008
1176
  };
1009
1177
  var WalletUtils = class _WalletUtils {
1010
1178
  static detectChainType(address) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spaps-sdk",
3
- "version": "1.2.0",
4
- "description": "Sweet Potato Authentication & Payment Service SDK - Zero-config client with built-in permission checking and role-based access control",
3
+ "version": "1.3.0",
4
+ "description": "Sweet Potato Authentication & Payment Service SDK - Zero-config client with built-in permission checking, role-based access control, and dayrate scheduling",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -31,7 +31,9 @@
31
31
  "sdk",
32
32
  "client",
33
33
  "sweet-potato",
34
- "wallet-auth"
34
+ "wallet-auth",
35
+ "dayrate",
36
+ "scheduling"
35
37
  ],
36
38
  "author": "buildooor",
37
39
  "license": "UNLICENSED",
@@ -44,7 +46,7 @@
44
46
  "email": "buildooor@gmail.com"
45
47
  },
46
48
  "dependencies": {
47
- "spaps-types": "^1.0.58",
49
+ "spaps-types": "^1.0.59",
48
50
  "axios": "^1.6.0",
49
51
  "cross-fetch": "^4.0.0"
50
52
  },