nodestack-client 2.0.0 → 2.2.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 +52 -0
- package/dist/Client.d.ts +1 -0
- package/dist/Client.d.ts.map +1 -1
- package/dist/auth/PkceUtil.d.ts +9 -0
- package/dist/auth/PkceUtil.d.ts.map +1 -0
- package/dist/index.cjs +261 -3
- package/dist/index.cjs.map +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +261 -3
- package/dist/index.mjs.map +3 -3
- package/dist/services/RecordService.d.ts +18 -1
- package/dist/services/RecordService.d.ts.map +1 -1
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +50 -50
package/dist/index.mjs
CHANGED
|
@@ -196,7 +196,6 @@ var BaseService = class {
|
|
|
196
196
|
constructor(client) {
|
|
197
197
|
this.client = client;
|
|
198
198
|
}
|
|
199
|
-
client;
|
|
200
199
|
send(path, options = {}) {
|
|
201
200
|
return this.client.send(path, options);
|
|
202
201
|
}
|
|
@@ -208,7 +207,6 @@ var RecordService = class extends BaseService {
|
|
|
208
207
|
super(client);
|
|
209
208
|
this.collectionName = collectionName;
|
|
210
209
|
}
|
|
211
|
-
collectionName;
|
|
212
210
|
get clientWithRealtime() {
|
|
213
211
|
return this.client;
|
|
214
212
|
}
|
|
@@ -340,6 +338,175 @@ var RecordService = class extends BaseService {
|
|
|
340
338
|
}
|
|
341
339
|
return result;
|
|
342
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Returns a list of configured and enabled OAuth2 authentication providers.
|
|
343
|
+
*/
|
|
344
|
+
async listAuthMethods(redirectUri) {
|
|
345
|
+
return this.send(
|
|
346
|
+
`/api/collections/${encodeURIComponent(this.collectionName)}/auth-methods`,
|
|
347
|
+
{
|
|
348
|
+
method: "GET",
|
|
349
|
+
query: redirectUri ? { redirectUri } : void 0
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Authenticates against an auth collection using OAuth2 with PKCE code exchange.
|
|
355
|
+
* On success, automatically persists the token and record to authStore.
|
|
356
|
+
*/
|
|
357
|
+
async authWithOAuth2(options) {
|
|
358
|
+
let code = options.code;
|
|
359
|
+
let codeVerifier = options.codeVerifier;
|
|
360
|
+
if ((options.urlCallback || options.popup || typeof window !== "undefined" && !code && !codeVerifier) && (!code || !codeVerifier)) {
|
|
361
|
+
const authMethods = await this.listAuthMethods(options.redirectUrl);
|
|
362
|
+
const provider = authMethods.authProviders.find(
|
|
363
|
+
(p) => p.name.toLowerCase() === options.provider.toLowerCase()
|
|
364
|
+
);
|
|
365
|
+
if (!provider) {
|
|
366
|
+
throw new Error(`OAuth provider '${options.provider}' is not enabled or configured.`);
|
|
367
|
+
}
|
|
368
|
+
codeVerifier = provider.codeVerifier;
|
|
369
|
+
const expectedState = provider.state;
|
|
370
|
+
let expectedOrigin = null;
|
|
371
|
+
if (typeof window !== "undefined" && window.location) {
|
|
372
|
+
if (options.redirectUrl) {
|
|
373
|
+
try {
|
|
374
|
+
expectedOrigin = new URL(options.redirectUrl, window.location.href).origin;
|
|
375
|
+
} catch {
|
|
376
|
+
expectedOrigin = window.location.origin;
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
expectedOrigin = window.location.origin;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
let popup = options.popup || null;
|
|
383
|
+
if (options.urlCallback) {
|
|
384
|
+
const cbResult = options.urlCallback(provider.authUrl);
|
|
385
|
+
if (!popup && cbResult && typeof cbResult.postMessage === "function") {
|
|
386
|
+
popup = cbResult;
|
|
387
|
+
}
|
|
388
|
+
} else if (typeof window !== "undefined" && typeof window.open === "function") {
|
|
389
|
+
popup = window.open(provider.authUrl, "_blank", "width=600,height=700");
|
|
390
|
+
if (!popup) {
|
|
391
|
+
throw new Error("Failed to open OAuth popup window. It may have been blocked by the browser.");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return new Promise((resolve, reject) => {
|
|
395
|
+
if (typeof window !== "undefined" && window.addEventListener) {
|
|
396
|
+
let pollTimer = null;
|
|
397
|
+
const cleanup = () => {
|
|
398
|
+
if (pollTimer) {
|
|
399
|
+
clearInterval(pollTimer);
|
|
400
|
+
pollTimer = null;
|
|
401
|
+
}
|
|
402
|
+
if (typeof window !== "undefined" && window.removeEventListener) {
|
|
403
|
+
window.removeEventListener("message", handler);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const handler = async (event) => {
|
|
407
|
+
if (!event.data || typeof event.data !== "object" || event.data.type !== "nodestack_oauth_callback") {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (expectedOrigin && event.origin !== expectedOrigin) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (popup) {
|
|
414
|
+
if (event.source !== popup) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
} else if (event.source === window) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (expectedState && event.data.state !== expectedState) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
cleanup();
|
|
424
|
+
if (popup && typeof popup.close === "function") {
|
|
425
|
+
try {
|
|
426
|
+
popup.close();
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (event.data.error) {
|
|
431
|
+
const errMsg = typeof event.data.error === "string" ? event.data.error : event.data.error.message || event.data.error_description || JSON.stringify(event.data.error);
|
|
432
|
+
return reject(new Error(errMsg));
|
|
433
|
+
}
|
|
434
|
+
if (!event.data.code) {
|
|
435
|
+
return reject(new Error("OAuth callback message is missing authorization code."));
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
const res = await this.authWithOAuth2({
|
|
439
|
+
provider: options.provider,
|
|
440
|
+
code: event.data.code,
|
|
441
|
+
codeVerifier,
|
|
442
|
+
redirectUrl: options.redirectUrl,
|
|
443
|
+
createData: options.createData,
|
|
444
|
+
state: expectedState
|
|
445
|
+
});
|
|
446
|
+
resolve(res);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
reject(err);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
window.addEventListener("message", handler);
|
|
452
|
+
if (popup) {
|
|
453
|
+
pollTimer = setInterval(() => {
|
|
454
|
+
if (popup && popup.closed) {
|
|
455
|
+
cleanup();
|
|
456
|
+
reject(new Error("The OAuth popup window was closed before completing authentication."));
|
|
457
|
+
}
|
|
458
|
+
}, 500);
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
reject(new Error("urlCallback requires code and codeVerifier in non-browser environments."));
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (!code || !codeVerifier) {
|
|
466
|
+
throw new Error("Both code and codeVerifier are required for OAuth2 authentication");
|
|
467
|
+
}
|
|
468
|
+
const result = await this.send(
|
|
469
|
+
`/api/collections/${encodeURIComponent(this.collectionName)}/auth-with-oauth2`,
|
|
470
|
+
{
|
|
471
|
+
method: "POST",
|
|
472
|
+
body: {
|
|
473
|
+
provider: options.provider,
|
|
474
|
+
code,
|
|
475
|
+
codeVerifier,
|
|
476
|
+
state: options.state,
|
|
477
|
+
redirectUrl: options.redirectUrl,
|
|
478
|
+
createData: options.createData
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
if (result && result.token) {
|
|
483
|
+
this.client.authStore.save(result.token, result.record);
|
|
484
|
+
}
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Lists external social identities linked to a specific record.
|
|
489
|
+
*/
|
|
490
|
+
async listExternalAuths(recordId) {
|
|
491
|
+
return this.send(
|
|
492
|
+
`/api/collections/${encodeURIComponent(this.collectionName)}/records/${encodeURIComponent(recordId)}/external-auths`,
|
|
493
|
+
{
|
|
494
|
+
method: "GET"
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Unlinks an external social identity from a record.
|
|
500
|
+
*/
|
|
501
|
+
async unlinkExternalAuth(recordId, provider) {
|
|
502
|
+
await this.send(
|
|
503
|
+
`/api/collections/${encodeURIComponent(this.collectionName)}/records/${encodeURIComponent(recordId)}/external-auths/${encodeURIComponent(provider)}`,
|
|
504
|
+
{
|
|
505
|
+
method: "DELETE"
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
343
510
|
/**
|
|
344
511
|
* Exports records from the collection as CSV or JSON.
|
|
345
512
|
*/
|
|
@@ -1127,6 +1294,7 @@ var AppService = class extends BaseService {
|
|
|
1127
1294
|
var NodeStackClient = class _NodeStackClient {
|
|
1128
1295
|
baseUrl;
|
|
1129
1296
|
authStore;
|
|
1297
|
+
apiKey;
|
|
1130
1298
|
files;
|
|
1131
1299
|
realtime;
|
|
1132
1300
|
admins;
|
|
@@ -1144,6 +1312,7 @@ var NodeStackClient = class _NodeStackClient {
|
|
|
1144
1312
|
constructor(baseUrl = "/", options = {}) {
|
|
1145
1313
|
this.baseUrl = (baseUrl || "/").replace(/\/+$/, "");
|
|
1146
1314
|
this.authStore = options.authStore || new LocalAuthStore();
|
|
1315
|
+
this.apiKey = options.apiKey;
|
|
1147
1316
|
this.customFetch = options.fetch;
|
|
1148
1317
|
this.defaultHeaders = options.headers || {};
|
|
1149
1318
|
this.defaultTimeout = options.timeout || 12e4;
|
|
@@ -1207,7 +1376,9 @@ var NodeStackClient = class _NodeStackClient {
|
|
|
1207
1376
|
...this.defaultHeaders,
|
|
1208
1377
|
...options.headers || {}
|
|
1209
1378
|
};
|
|
1210
|
-
if (this.
|
|
1379
|
+
if (this.apiKey && !headers["Authorization"] && !headers["authorization"] && !headers["x-api-key"] && !headers["X-API-Key"]) {
|
|
1380
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1381
|
+
} else if (this.authStore.token && !headers["Authorization"] && !headers["authorization"]) {
|
|
1211
1382
|
headers["Authorization"] = `Bearer ${this.authStore.token}`;
|
|
1212
1383
|
}
|
|
1213
1384
|
if (this.appSlug && !headers["x-nodestack-app"] && !headers["X-NodeStack-App"]) {
|
|
@@ -1350,6 +1521,92 @@ var NodeStackClient = class _NodeStackClient {
|
|
|
1350
1521
|
var MemoryAuthStore = class extends BaseAuthStore {
|
|
1351
1522
|
// Inherits default in-memory behavior of BaseAuthStore
|
|
1352
1523
|
};
|
|
1524
|
+
|
|
1525
|
+
// src/auth/PkceUtil.ts
|
|
1526
|
+
var PkceUtil = class _PkceUtil {
|
|
1527
|
+
static generateVerifier(length = 64) {
|
|
1528
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
1529
|
+
const clampedLength = Math.max(43, Math.min(128, length));
|
|
1530
|
+
const randomBytes = new Uint8Array(clampedLength);
|
|
1531
|
+
const g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1532
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
1533
|
+
crypto.getRandomValues(randomBytes);
|
|
1534
|
+
} else if (g.crypto && g.crypto.getRandomValues) {
|
|
1535
|
+
g.crypto.getRandomValues(randomBytes);
|
|
1536
|
+
} else {
|
|
1537
|
+
let filled = false;
|
|
1538
|
+
try {
|
|
1539
|
+
const req = g.require;
|
|
1540
|
+
if (typeof req === "function") {
|
|
1541
|
+
req("crypto").randomFillSync(randomBytes);
|
|
1542
|
+
filled = true;
|
|
1543
|
+
}
|
|
1544
|
+
} catch {
|
|
1545
|
+
}
|
|
1546
|
+
if (!filled) {
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
"A cryptographically secure random number generator (crypto.getRandomValues or node:crypto) is required to generate a PKCE code_verifier."
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
let result = "";
|
|
1553
|
+
for (let i = 0; i < clampedLength; i++) {
|
|
1554
|
+
result += charset[randomBytes[i] % charset.length];
|
|
1555
|
+
}
|
|
1556
|
+
return result;
|
|
1557
|
+
}
|
|
1558
|
+
static async generateChallenge(verifier) {
|
|
1559
|
+
const encoder = new TextEncoder();
|
|
1560
|
+
const data = encoder.encode(verifier);
|
|
1561
|
+
const g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1562
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
1563
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
1564
|
+
return _PkceUtil.base64UrlEncode(new Uint8Array(hash));
|
|
1565
|
+
} else if (g.crypto && g.crypto.subtle) {
|
|
1566
|
+
const hash = await g.crypto.subtle.digest("SHA-256", data);
|
|
1567
|
+
return _PkceUtil.base64UrlEncode(new Uint8Array(hash));
|
|
1568
|
+
}
|
|
1569
|
+
try {
|
|
1570
|
+
const req = g.require;
|
|
1571
|
+
if (typeof req === "function") {
|
|
1572
|
+
const hash = req("crypto").createHash("sha256").update(data).digest();
|
|
1573
|
+
return _PkceUtil.base64UrlEncode(new Uint8Array(hash));
|
|
1574
|
+
}
|
|
1575
|
+
} catch {
|
|
1576
|
+
}
|
|
1577
|
+
throw new Error("Web Crypto API (crypto.subtle) is not available to compute SHA-256 code challenge.");
|
|
1578
|
+
}
|
|
1579
|
+
static base64UrlEncode(bytes) {
|
|
1580
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1581
|
+
let base64 = "";
|
|
1582
|
+
const byteLength = bytes.byteLength;
|
|
1583
|
+
const byteRemainder = byteLength % 3;
|
|
1584
|
+
const mainLength = byteLength - byteRemainder;
|
|
1585
|
+
let a, b, c, d;
|
|
1586
|
+
let chunk;
|
|
1587
|
+
for (let i = 0; i < mainLength; i += 3) {
|
|
1588
|
+
chunk = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
|
|
1589
|
+
a = (chunk & 16515072) >> 18;
|
|
1590
|
+
b = (chunk & 258048) >> 12;
|
|
1591
|
+
c = (chunk & 4032) >> 6;
|
|
1592
|
+
d = chunk & 63;
|
|
1593
|
+
base64 += chars[a] + chars[b] + chars[c] + chars[d];
|
|
1594
|
+
}
|
|
1595
|
+
if (byteRemainder === 1) {
|
|
1596
|
+
chunk = bytes[mainLength];
|
|
1597
|
+
a = (chunk & 252) >> 2;
|
|
1598
|
+
b = (chunk & 3) << 4;
|
|
1599
|
+
base64 += chars[a] + chars[b] + "==";
|
|
1600
|
+
} else if (byteRemainder === 2) {
|
|
1601
|
+
chunk = bytes[mainLength] << 8 | bytes[mainLength + 1];
|
|
1602
|
+
a = (chunk & 64512) >> 10;
|
|
1603
|
+
b = (chunk & 1008) >> 4;
|
|
1604
|
+
c = (chunk & 15) << 2;
|
|
1605
|
+
base64 += chars[a] + chars[b] + chars[c] + "=";
|
|
1606
|
+
}
|
|
1607
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1353
1610
|
export {
|
|
1354
1611
|
AdminService,
|
|
1355
1612
|
AppService,
|
|
@@ -1362,6 +1619,7 @@ export {
|
|
|
1362
1619
|
LocalAuthStore,
|
|
1363
1620
|
MemoryAuthStore,
|
|
1364
1621
|
NodeStackClient,
|
|
1622
|
+
PkceUtil,
|
|
1365
1623
|
RealtimeService,
|
|
1366
1624
|
RecordService,
|
|
1367
1625
|
SnapshotService,
|