lazypock 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,6 +57,10 @@ const user = await client.collection('users').create({
57
57
  const session = await client.authWithPassword('users', 'ada@example.com', 'correct-horse-battery');
58
58
  // session.token — stored in client.authStore for subsequent requests
59
59
 
60
+ // OAuth2 login — one call: opens the provider popup and stores the session
61
+ const oauth = await client.collection('users').authWithOAuth2({ provider: 'google' });
62
+ // oauth.token + oauth.record + oauth.meta (isNew, email, avatarURL, …)
63
+
60
64
  // File upload
61
65
  const file = await client.files.upload(fileInput.files[0]);
62
66
 
@@ -399,7 +403,9 @@ Returned by `client.collection(name)`.
399
403
  - `unsubscribe(topic?)` — `unsubscribe('*')` / `unsubscribe('id')` / `unsubscribe()` (all)
400
404
  - `authWithPassword(identity, password, options?)` — Login to this auth collection
401
405
  - `authRefresh(options?)` — Refresh token for this auth collection
402
- - `authMethods(options?)` — Get available auth methods
406
+ - `listAuthMethods(options?)` — Get available auth methods (password + OAuth2 providers)
407
+ - `authWithOAuth2(options)` — Sign in with an OAuth2 provider (popup flow)
408
+ - `authWithOAuth2Code(options)` — Exchange an OAuth2 authorization code directly
403
409
 
404
410
  ### AuthStore
405
411
 
package/dist/index.cjs CHANGED
@@ -44,12 +44,13 @@ module.exports = __toCommonJS(index_exports);
44
44
 
45
45
  // src/types.ts
46
46
  var ApiError = class extends Error {
47
- constructor(message, data, status, isAbort = false) {
47
+ constructor(message, data, status, isAbort = false, code) {
48
48
  super(message);
49
49
  this.name = "ApiError";
50
50
  this.data = data;
51
51
  this.status = status;
52
52
  this.isAbort = isAbort;
53
+ this.code = code;
53
54
  }
54
55
  };
55
56
 
@@ -80,11 +81,15 @@ var HttpClient = class {
80
81
  this.inflight = {};
81
82
  /** Global toggle for the auto-cancellation behaviour (default: on). */
82
83
  this.enableAutoCancellation = true;
83
- this.baseUrl = baseUrl.replace(/\/+$/, "");
84
+ this._baseUrl = baseUrl.replace(/\/+$/, "");
84
85
  this.authStore = authStore;
85
86
  this.connectionId = connectionId;
86
87
  this.defaultFetch = globalThis.fetch.bind(globalThis);
87
88
  }
89
+ /** The API base URL (trailing slash stripped). Used for OAuth2 origin checks. */
90
+ get baseUrl() {
91
+ return this._baseUrl;
92
+ }
88
93
  async refreshAuth() {
89
94
  const collection = this.authStore.collectionName;
90
95
  if (!collection) return null;
@@ -470,6 +475,202 @@ var memoryStorage = {
470
475
  }
471
476
  };
472
477
 
478
+ // src/oauth2.ts
479
+ var DEFAULT_POPUP_WIDTH = 500;
480
+ var DEFAULT_POPUP_HEIGHT = 700;
481
+ var DEFAULT_OAUTH2_TIMEOUT_MS = 12e4;
482
+ var CLOSE_POLL_MS = 500;
483
+ function oauth2RedirectOrigin(baseUrl) {
484
+ try {
485
+ return new URL(baseUrl).origin;
486
+ } catch {
487
+ throw new ApiError(
488
+ `Invalid baseUrl for OAuth2 origin check: "${baseUrl}"`,
489
+ {},
490
+ 0,
491
+ false,
492
+ "oauth2_invalid_base_url"
493
+ );
494
+ }
495
+ }
496
+ function isSafeBrowserUrl(url) {
497
+ try {
498
+ const protocol = new URL(url).protocol;
499
+ return protocol === "http:" || protocol === "https:";
500
+ } catch {
501
+ return false;
502
+ }
503
+ }
504
+ function isRecordAuth(value) {
505
+ return !!value && typeof value === "object" && typeof value.token === "string" && !!value.record && typeof value.record === "object";
506
+ }
507
+ function authorizeWithOAuth2Popup(opts) {
508
+ return new Promise((resolve, reject) => {
509
+ if (typeof window === "undefined") {
510
+ reject(
511
+ new ApiError(
512
+ "OAuth2 popup flow requires a browser window. Use `authWithOAuth2Code()` in non-browser environments.",
513
+ {},
514
+ 0,
515
+ false,
516
+ "oauth2_popup_unavailable"
517
+ )
518
+ );
519
+ return;
520
+ }
521
+ let settled = false;
522
+ let popup = null;
523
+ let closePoll = null;
524
+ let timeout = null;
525
+ const cleanup = () => {
526
+ if (timeout) {
527
+ clearTimeout(timeout);
528
+ timeout = null;
529
+ }
530
+ if (closePoll) {
531
+ clearInterval(closePoll);
532
+ closePoll = null;
533
+ }
534
+ window.removeEventListener("message", onMessage);
535
+ };
536
+ const finish = (err, result) => {
537
+ if (settled) return;
538
+ settled = true;
539
+ cleanup();
540
+ if (err) reject(err);
541
+ else resolve(result);
542
+ };
543
+ const onMessage = (event) => {
544
+ if (event.origin !== opts.expectedOrigin) return;
545
+ if (popup && event.source && event.source !== popup) return;
546
+ const data = event.data;
547
+ if (!data || typeof data !== "object") return;
548
+ if (data.type === "lazypock:oauth2") {
549
+ if (isRecordAuth(data.result)) {
550
+ if (popup) {
551
+ try {
552
+ popup.close();
553
+ } catch {
554
+ }
555
+ }
556
+ finish(null, data.result);
557
+ } else {
558
+ finish(
559
+ new ApiError(
560
+ "OAuth2 callback returned an invalid result.",
561
+ data,
562
+ 400,
563
+ false,
564
+ "oauth2_provider_error"
565
+ )
566
+ );
567
+ }
568
+ return;
569
+ }
570
+ if (data.type === "lazypock:oauth2:error") {
571
+ const result = data.result ?? {};
572
+ const message = typeof result.message === "string" && result.message ? result.message : "OAuth2 authorization failed.";
573
+ finish(
574
+ new ApiError(
575
+ message,
576
+ result,
577
+ typeof result.code === "number" ? result.code : 400,
578
+ false,
579
+ "oauth2_provider_error"
580
+ )
581
+ );
582
+ }
583
+ };
584
+ const openPopup = () => {
585
+ if (!isSafeBrowserUrl(opts.authURL)) {
586
+ finish(
587
+ new ApiError(
588
+ "OAuth2 authorization URL has an unsupported protocol.",
589
+ {},
590
+ 0,
591
+ false,
592
+ "oauth2_provider_error"
593
+ )
594
+ );
595
+ return;
596
+ }
597
+ const width = opts.popup?.width ?? DEFAULT_POPUP_WIDTH;
598
+ const height = opts.popup?.height ?? DEFAULT_POPUP_HEIGHT;
599
+ const features = "width=" + width + ",height=" + height + ",scrollbars=yes,resizable=yes";
600
+ const win = window.open(opts.authURL, "_blank", features);
601
+ if (!win) {
602
+ finish(
603
+ new ApiError(
604
+ "OAuth2 popup was blocked by the browser. Please allow popups for this site and try again.",
605
+ {},
606
+ 0,
607
+ false,
608
+ "oauth2_popup_blocked"
609
+ )
610
+ );
611
+ return;
612
+ }
613
+ popup = win;
614
+ closePoll = setInterval(() => {
615
+ if (popup && popup.closed) {
616
+ finish(
617
+ new ApiError(
618
+ "OAuth2 sign-in was cancelled.",
619
+ {},
620
+ 0,
621
+ false,
622
+ "oauth2_cancelled"
623
+ )
624
+ );
625
+ }
626
+ }, CLOSE_POLL_MS);
627
+ };
628
+ window.addEventListener("message", onMessage);
629
+ timeout = setTimeout(() => {
630
+ if (popup) {
631
+ try {
632
+ popup.close();
633
+ } catch {
634
+ }
635
+ }
636
+ finish(
637
+ new ApiError(
638
+ "OAuth2 sign-in timed out.",
639
+ {},
640
+ 0,
641
+ false,
642
+ "oauth2_timeout"
643
+ )
644
+ );
645
+ }, opts.timeoutMs);
646
+ if (opts.urlCallback) {
647
+ Promise.resolve(opts.urlCallback(opts.authURL)).catch((err) => {
648
+ finish(
649
+ err instanceof ApiError ? err : new ApiError(
650
+ `OAuth2 urlCallback failed: ${String(err)}`,
651
+ {},
652
+ 0,
653
+ false,
654
+ "oauth2_provider_error"
655
+ )
656
+ );
657
+ });
658
+ } else if (typeof window.open === "function") {
659
+ openPopup();
660
+ } else {
661
+ finish(
662
+ new ApiError(
663
+ "OAuth2 popup flow requires a browser window. Use `authWithOAuth2Code()` in non-browser environments.",
664
+ {},
665
+ 0,
666
+ false,
667
+ "oauth2_popup_unavailable"
668
+ )
669
+ );
670
+ }
671
+ });
672
+ }
673
+
473
674
  // src/collection.ts
474
675
  function stableStringify(value) {
475
676
  const seen = /* @__PURE__ */ new Set();
@@ -904,15 +1105,108 @@ var CollectionService = class _CollectionService {
904
1105
  return data;
905
1106
  }
906
1107
  /**
907
- * Get available auth methods for this collection.
1108
+ * Get available auth methods for this collection (PocketBase `listAuthMethods`).
1109
+ * Returns `password`, `oauth2.providers` (each with `name`, `authURL`,
1110
+ * `state`, `codeVerifier`), and `mfa`.
908
1111
  */
909
- // ── end Realtime ──
910
- async authMethods(options) {
1112
+ async listAuthMethods(options) {
911
1113
  return this.http.get(
912
1114
  "/" + this.encodeId(this.collectionName) + "/auth-methods",
913
1115
  options
914
1116
  );
915
1117
  }
1118
+ /**
1119
+ * Get available auth methods for this collection.
1120
+ * @deprecated Use {@link listAuthMethods} (typed).
1121
+ */
1122
+ authMethods(options) {
1123
+ return this.listAuthMethods(options);
1124
+ }
1125
+ /**
1126
+ * Sign in with an OAuth2 provider (PocketBase `authWithOAuth2` parity).
1127
+ *
1128
+ * One call handles the whole web flow: fetch the provider's `authURL`,
1129
+ * open a popup (or call `options.urlCallback`), wait for the backend's
1130
+ * `postMessage` result, populate the auth store, and resolve with
1131
+ * `{ token, record, meta }`.
1132
+ *
1133
+ * `options.createData` is accepted for PocketBase signature parity but is
1134
+ * **not** forwarded by the backend's popup redirect flow — use
1135
+ * {@link authWithOAuth2Code} when you need extra fields on first sign-up.
1136
+ *
1137
+ * React Native / non-browser: the popup flow depends on
1138
+ * `window.postMessage`. Use {@link listAuthMethods} + {@link authWithOAuth2Code}
1139
+ * instead.
1140
+ */
1141
+ async authWithOAuth2(options) {
1142
+ const {
1143
+ provider,
1144
+ urlCallback,
1145
+ popup,
1146
+ timeoutMs = DEFAULT_OAUTH2_TIMEOUT_MS
1147
+ } = options;
1148
+ const methods = await this.listAuthMethods();
1149
+ const providerInfo = methods?.oauth2?.providers?.find(
1150
+ (p) => p.name === provider
1151
+ );
1152
+ if (!providerInfo) {
1153
+ throw new ApiError(
1154
+ `OAuth2 provider "${provider}" is not enabled for this collection.`,
1155
+ methods ?? {},
1156
+ 400,
1157
+ false,
1158
+ "oauth2_unknown_provider"
1159
+ );
1160
+ }
1161
+ const result = await authorizeWithOAuth2Popup({
1162
+ authURL: providerInfo.authURL,
1163
+ expectedOrigin: oauth2RedirectOrigin(this.http.baseUrl),
1164
+ urlCallback,
1165
+ popup,
1166
+ timeoutMs
1167
+ });
1168
+ if (this.authStore) {
1169
+ this.authStore.setCollectionName(this.collectionName);
1170
+ this.authStore.set(result.token, result.record);
1171
+ }
1172
+ return result;
1173
+ }
1174
+ /**
1175
+ * Exchange an OAuth2 authorization code directly (PocketBase
1176
+ * `authWithOAuth2Code` parity).
1177
+ *
1178
+ * Used by mobile / non-browser flows: fetch {@link listAuthMethods} to get
1179
+ * the provider `authURL` + `codeVerifier`, present the URL yourself,
1180
+ * capture the `code` from your redirect/deep link, then call this to
1181
+ * finish and populate the auth store.
1182
+ */
1183
+ async authWithOAuth2Code(options) {
1184
+ const { provider, code, codeVerifier, redirectUrl, createData } = options;
1185
+ const data = await this.http.post(
1186
+ "/" + this.encodeId(this.collectionName) + "/auth-with-oauth2",
1187
+ {
1188
+ provider,
1189
+ code,
1190
+ codeVerifier,
1191
+ redirectUrl: redirectUrl ?? this.http.baseUrl + "/oauth2-redirect",
1192
+ createData: createData ?? {}
1193
+ }
1194
+ );
1195
+ if (!data) {
1196
+ throw new ApiError(
1197
+ "OAuth2 code exchange returned no data.",
1198
+ {},
1199
+ 0,
1200
+ false,
1201
+ "oauth2_exchange_failed"
1202
+ );
1203
+ }
1204
+ if (this.authStore) {
1205
+ this.authStore.setCollectionName(this.collectionName);
1206
+ this.authStore.set(data.token, data.record);
1207
+ }
1208
+ return data;
1209
+ }
916
1210
  };
917
1211
 
918
1212
  // src/realtime.ts