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 +7 -1
- package/dist/index.cjs +299 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -4
- package/dist/index.d.ts +148 -4
- package/dist/index.global.js +299 -5
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +299 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/collection.ts +140 -17
- package/src/http.ts +7 -2
- package/src/index.ts +9 -0
- package/src/lazypock.ts +10 -0
- package/src/oauth2.ts +397 -0
- package/src/types.ts +15 -2
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
|
-
- `
|
|
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.
|
|
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
|
-
|
|
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
|