@getstrata/core 1.0.8 → 1.1.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/CHANGELOG.md +52 -0
- package/dist/core/auth/authContext.d.ts +5 -2
- package/dist/core/auth/emailVerification.d.ts +1 -1
- package/dist/core/auth/guard.d.ts +5 -1
- package/dist/core/auth/jwtGuard.d.ts +6 -2
- package/dist/core/auth/oauth/oidcIdToken.d.ts +19 -0
- package/dist/core/auth/oauth/oidcProvider.d.ts +16 -4
- package/dist/core/auth/oauth/samlProvider.d.ts +7 -4
- package/dist/core/auth/oneTimeToken.d.ts +19 -0
- package/dist/core/auth/passwordLogin.d.ts +20 -0
- package/dist/core/auth/saml/samlAssertionReplay.d.ts +20 -0
- package/dist/core/auth/saml/samlServiceProvider.d.ts +51 -0
- package/dist/core/contracts/authUserDirectory.d.ts +2 -3
- package/dist/core/database/errors.d.ts +2 -1
- package/dist/core/http/response.d.ts +3 -1
- package/dist/core/mail/mailer.d.ts +6 -1
- package/dist/core/security/oauthState.d.ts +6 -2
- package/dist/core/security/safeFetch.d.ts +1 -0
- package/dist/core/security/safePath.d.ts +4 -0
- package/dist/core/security/safeUrl.d.ts +14 -1
- package/dist/core/security/totp.d.ts +1 -1
- package/dist/core/tenant/databaseTenantContext.d.ts +2 -1
- package/dist/core/tenant/enableTenantRls.d.ts +6 -0
- package/dist/core/tenant/postgresAppRole.d.ts +75 -0
- package/dist/entries/audit/exportAuditLogs.js +223 -40
- package/dist/entries/auth/accessControl.js +1 -0
- package/dist/entries/auth/basicAuthGuard.js +638 -17
- package/dist/entries/auth/emailVerification.js +1 -1
- package/dist/entries/auth/intendedUrlCookie.js +32 -2
- package/dist/entries/auth/jwt.js +1 -1
- package/dist/entries/auth/jwtGuard.js +192 -3
- package/dist/entries/auth/membershipMiddleware.js +1 -0
- package/dist/entries/auth/membershipScope.js +1 -0
- package/dist/entries/auth/membershipService.js +1 -0
- package/dist/entries/auth/oauth/oidcProvider.js +542 -20
- package/dist/entries/auth/oauth/providers.js +313 -5
- package/dist/entries/auth/oauth/samlProvider.js +552 -74
- package/dist/entries/auth/oneTimeToken.js +362 -0
- package/dist/entries/auth/password.js +1 -1
- package/dist/entries/auth/passwordLogin.js +548 -0
- package/dist/entries/auth/policy.js +1 -0
- package/dist/entries/auth/saml/samlServiceProvider.js +571 -0
- package/dist/entries/auth/scimAuthMiddleware.js +10 -3
- package/dist/entries/auth/sessionCookie.js +1 -1
- package/dist/entries/auth/sessionGuard.js +1 -1
- package/dist/entries/auth/tokenHash.js +2 -6
- package/dist/entries/crypto/fieldEncryption.js +1 -1
- package/dist/entries/crypto/mfaSecret.js +31 -5
- package/dist/entries/database/errors.js +29 -12
- package/dist/entries/database/model.js +9 -9
- package/dist/entries/facades.js +72 -9
- package/dist/entries/http/authMiddleware.js +7 -14
- package/dist/entries/http/corsMiddleware.js +69 -2
- package/dist/entries/http/csrfMiddleware.js +33 -43
- package/dist/entries/http/csrfToken.js +2 -2
- package/dist/entries/http/flashMiddleware.js +4 -2
- package/dist/entries/http/flashSession.js +4 -2
- package/dist/entries/http/formRequest.js +2 -2
- package/dist/entries/http/memoryThrottleMiddleware.js +63 -0
- package/dist/entries/http/pagination.js +2 -2
- package/dist/entries/http/requirePasswordConfirmMiddleware.js +36 -3
- package/dist/entries/http/requireWebAuthMiddleware.js +32 -2
- package/dist/entries/http/response.js +101 -45
- package/dist/entries/http/routeModelBinding.js +2 -2
- package/dist/entries/http/safeInternalPath.js +32 -2
- package/dist/entries/http/scimThrottleMiddleware.js +7 -1
- package/dist/entries/http/securedRouteModelBinding.js +2 -2
- package/dist/entries/http/securityHeadersMiddleware.js +3 -64
- package/dist/entries/http/signedUrl.js +6 -5
- package/dist/entries/http/throttleMiddleware.js +63 -0
- package/dist/entries/http/validation.js +2 -2
- package/dist/entries/http/webFormRequest.js +2 -2
- package/dist/entries/jobs/exportAuditLogsJob.js +223 -40
- package/dist/entries/mail/mailer.js +54 -8
- package/dist/entries/media/imageTransform.js +12 -1
- package/dist/entries/openapi/generator.js +0 -7
- package/dist/entries/security/oauthState.js +30 -39
- package/dist/entries/security/publicReads.js +1 -1
- package/dist/entries/security/recoveryCodes.js +2 -2
- package/dist/entries/security/safeFetch.js +195 -94
- package/dist/entries/security/safePath.js +25 -0
- package/dist/entries/security/scimTenantTokens.js +10 -3
- package/dist/entries/security/totp.js +16 -4
- package/dist/entries/storage/storage.js +24 -2
- package/dist/entries/tenant/databaseTenantContext.js +34 -12
- package/dist/entries/tenant/enableTenantRls.js +385 -0
- package/dist/entries/tracing/tracingMiddleware.js +308 -5
- package/dist/framework/public-api.d.ts +10 -3
- package/dist/index.js +2767 -1515
- package/package.json +32 -3
|
@@ -96,34 +96,144 @@ var BLOCKED_HOSTNAMES = new Set([
|
|
|
96
96
|
"::1",
|
|
97
97
|
"metadata.google.internal"
|
|
98
98
|
]);
|
|
99
|
-
function
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
99
|
+
function parseCanonicalDottedDecimal(hostname) {
|
|
100
|
+
const parts = hostname.split(".");
|
|
101
|
+
if (parts.length !== 4) {
|
|
102
|
+
return null;
|
|
103
103
|
}
|
|
104
|
-
const octets =
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const octets = [];
|
|
105
|
+
for (const part of parts) {
|
|
106
|
+
if (!/^(0|[1-9]\d{0,2})$/u.test(part)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const value = Number.parseInt(part, 10);
|
|
110
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
octets.push(value);
|
|
107
114
|
}
|
|
115
|
+
return octets;
|
|
116
|
+
}
|
|
117
|
+
function parseDottedIpv4(hostname) {
|
|
118
|
+
return parseCanonicalDottedDecimal(hostname);
|
|
119
|
+
}
|
|
120
|
+
function isBlockedIpv4Octets(octets) {
|
|
108
121
|
const [a = 0, b = 0] = octets;
|
|
109
|
-
if (a === 10) {
|
|
122
|
+
if (a === 10 || a === 127 || a === 0) {
|
|
110
123
|
return true;
|
|
111
124
|
}
|
|
112
|
-
if (a ===
|
|
125
|
+
if (a === 169 && b === 254) {
|
|
113
126
|
return true;
|
|
114
127
|
}
|
|
115
|
-
if (a ===
|
|
128
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
116
129
|
return true;
|
|
117
130
|
}
|
|
118
|
-
if (a ===
|
|
131
|
+
if (a === 192 && b === 168) {
|
|
119
132
|
return true;
|
|
120
133
|
}
|
|
121
|
-
if (a ===
|
|
134
|
+
if (a === 100 && b >= 64 && b <= 127) {
|
|
122
135
|
return true;
|
|
123
136
|
}
|
|
124
|
-
if (a ===
|
|
137
|
+
if (a === 198 && (b === 18 || b === 19)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function expandIpv6(hostname) {
|
|
143
|
+
const trimmed = hostname.trim().toLowerCase();
|
|
144
|
+
const unbracketed = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
145
|
+
if (unbracketed.includes("%")) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const mapped = unbracketed.match(/^::ffff:([0-9a-fx.]+)$/iu);
|
|
149
|
+
if (mapped?.[1]) {
|
|
150
|
+
const octets = parseDottedIpv4(mapped[1]);
|
|
151
|
+
if (!octets) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return [
|
|
155
|
+
0,
|
|
156
|
+
0,
|
|
157
|
+
0,
|
|
158
|
+
0,
|
|
159
|
+
0,
|
|
160
|
+
65535,
|
|
161
|
+
octets[0] << 8 | octets[1],
|
|
162
|
+
octets[2] << 8 | octets[3]
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
const halves = unbracketed.split("::");
|
|
166
|
+
if (halves.length > 2) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const parseGroup = (part) => {
|
|
170
|
+
if (!/^[0-9a-f]{1,4}$/u.test(part)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return Number.parseInt(part, 16);
|
|
174
|
+
};
|
|
175
|
+
if (halves.length === 1) {
|
|
176
|
+
const groups = unbracketed.split(":");
|
|
177
|
+
if (groups.length !== 8) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const values = groups.map(parseGroup);
|
|
181
|
+
return values.every((value) => value !== null) ? values : null;
|
|
182
|
+
}
|
|
183
|
+
const head = halves[0] ? halves[0].split(":").filter(Boolean) : [];
|
|
184
|
+
const tail = halves[1] ? halves[1].split(":").filter(Boolean) : [];
|
|
185
|
+
if (head.length + tail.length > 8) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const values = [];
|
|
189
|
+
for (const part of head) {
|
|
190
|
+
const value = parseGroup(part);
|
|
191
|
+
if (value === null) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
values.push(value);
|
|
195
|
+
}
|
|
196
|
+
while (values.length < 8 - tail.length) {
|
|
197
|
+
values.push(0);
|
|
198
|
+
}
|
|
199
|
+
for (const part of tail) {
|
|
200
|
+
const value = parseGroup(part);
|
|
201
|
+
if (value === null) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
values.push(value);
|
|
205
|
+
}
|
|
206
|
+
return values.length === 8 ? values : null;
|
|
207
|
+
}
|
|
208
|
+
function isBlockedIpv6(hostname) {
|
|
209
|
+
const groups = expandIpv6(hostname);
|
|
210
|
+
if (!groups) {
|
|
211
|
+
return hostname.includes(":");
|
|
212
|
+
}
|
|
213
|
+
const first = groups[0] ?? 0;
|
|
214
|
+
const second = groups[1] ?? 0;
|
|
215
|
+
if (groups.every((group) => group === 0)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if ((first & 65472) === 65152) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if ((first & 65024) === 64512) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if ((first & 65280) === 65280) {
|
|
125
228
|
return true;
|
|
126
229
|
}
|
|
230
|
+
if (first === 0 && second === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
|
|
231
|
+
const a = (groups[6] ?? 0) >> 8;
|
|
232
|
+
const b = (groups[6] ?? 0) & 255;
|
|
233
|
+
const c = (groups[7] ?? 0) >> 8;
|
|
234
|
+
const d = (groups[7] ?? 0) & 255;
|
|
235
|
+
return isBlockedIpv4Octets([a, b, c, d]);
|
|
236
|
+
}
|
|
127
237
|
return false;
|
|
128
238
|
}
|
|
129
239
|
function isBlockedHostname(hostname) {
|
|
@@ -138,24 +248,43 @@ function isBlockedHostname(hostname) {
|
|
|
138
248
|
return true;
|
|
139
249
|
}
|
|
140
250
|
if (normalized.includes(":")) {
|
|
251
|
+
return isBlockedIpv6(normalized);
|
|
252
|
+
}
|
|
253
|
+
if (/^\d+$/u.test(normalized)) {
|
|
141
254
|
return true;
|
|
142
255
|
}
|
|
143
|
-
|
|
256
|
+
if (normalized.includes(".")) {
|
|
257
|
+
const canonical = parseCanonicalDottedDecimal(normalized);
|
|
258
|
+
if (canonical) {
|
|
259
|
+
return isBlockedIpv4Octets(canonical);
|
|
260
|
+
}
|
|
261
|
+
if (/^[0-9a-fx.]+$/iu.test(normalized)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
function productionSafeOutboundOptions(options) {
|
|
268
|
+
if (!isProductionEnv()) {
|
|
269
|
+
return options;
|
|
270
|
+
}
|
|
271
|
+
return { ...options, allowPrivate: false, resolveDns: true };
|
|
144
272
|
}
|
|
145
273
|
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
274
|
+
const resolved = productionSafeOutboundOptions(options);
|
|
146
275
|
let parsed;
|
|
147
276
|
try {
|
|
148
277
|
parsed = new URL(rawUrl);
|
|
149
278
|
} catch {
|
|
150
279
|
throw new BadRequestError("Webhook URL is invalid.");
|
|
151
280
|
}
|
|
152
|
-
if (parsed.protocol !== "https:" && !(
|
|
281
|
+
if (parsed.protocol !== "https:" && !(resolved.allowHttp && parsed.protocol === "http:")) {
|
|
153
282
|
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
154
283
|
}
|
|
155
284
|
if (parsed.username || parsed.password) {
|
|
156
285
|
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
157
286
|
}
|
|
158
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
287
|
+
if (!resolved.allowPrivate && isBlockedHostname(parsed.hostname)) {
|
|
159
288
|
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
160
289
|
}
|
|
161
290
|
return parsed;
|
|
@@ -163,17 +292,35 @@ function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
|
163
292
|
function isBlockedIpAddress(address) {
|
|
164
293
|
return isBlockedHostname(address.trim().toLowerCase());
|
|
165
294
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
295
|
+
function isLiteralIpHostname(hostname) {
|
|
296
|
+
return parseCanonicalDottedDecimal(hostname) !== null || expandIpv6(hostname) !== null;
|
|
297
|
+
}
|
|
298
|
+
function pinUrlToAddress(url, address) {
|
|
299
|
+
const pinned = new URL(url.toString());
|
|
300
|
+
const ipv6 = expandIpv6(address.replace(/^\[|\]$/gu, ""));
|
|
301
|
+
pinned.hostname = ipv6 ? `[${address.replace(/^\[|\]$/gu, "")}]` : address;
|
|
302
|
+
return pinned;
|
|
303
|
+
}
|
|
304
|
+
async function resolveSafeOutboundTarget(rawUrl, options = {}) {
|
|
305
|
+
const resolvedOptions = productionSafeOutboundOptions(options);
|
|
306
|
+
const parsed = assertSafeOutboundUrl(rawUrl, resolvedOptions);
|
|
307
|
+
if (resolvedOptions.resolveDns === false || resolvedOptions.allowPrivate) {
|
|
308
|
+
return { url: parsed, addresses: [] };
|
|
170
309
|
}
|
|
171
310
|
const hostname = parsed.hostname.trim().toLowerCase();
|
|
172
|
-
|
|
173
|
-
|
|
311
|
+
if (isLiteralIpHostname(hostname)) {
|
|
312
|
+
return { url: parsed, addresses: [hostname] };
|
|
313
|
+
}
|
|
314
|
+
let results;
|
|
315
|
+
try {
|
|
316
|
+
results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
317
|
+
} catch {
|
|
174
318
|
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
175
319
|
}
|
|
176
|
-
|
|
320
|
+
if (results.length === 0 || results.some((result) => isBlockedIpAddress(result.address))) {
|
|
321
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
322
|
+
}
|
|
323
|
+
return { url: parsed, addresses: results.map((result) => result.address) };
|
|
177
324
|
}
|
|
178
325
|
|
|
179
326
|
// ../../src/core/security/safeFetch.ts
|
|
@@ -181,25 +328,40 @@ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
|
181
328
|
async function safeFetch(input, init = {}, options = {}) {
|
|
182
329
|
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
183
330
|
const maxRedirects = options.maxRedirects ?? 0;
|
|
184
|
-
const resolveDns = options.resolveDns ??
|
|
185
|
-
const urlOptions = {
|
|
331
|
+
const resolveDns = options.resolveDns ?? true;
|
|
332
|
+
const urlOptions = {
|
|
333
|
+
allowHttp: options.allowHttp,
|
|
334
|
+
resolveDns,
|
|
335
|
+
allowPrivate: options.allowPrivate
|
|
336
|
+
};
|
|
186
337
|
const controller = new AbortController;
|
|
187
338
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
188
339
|
try {
|
|
189
|
-
let
|
|
340
|
+
let current = await resolveSafeOutboundTarget(input, urlOptions);
|
|
190
341
|
let redirectCount = 0;
|
|
191
342
|
while (true) {
|
|
192
|
-
const
|
|
343
|
+
const address = current.addresses[0];
|
|
344
|
+
const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
|
|
345
|
+
const headers = new Headers(init.headers);
|
|
346
|
+
if (address && !headers.has("host")) {
|
|
347
|
+
headers.set("Host", current.url.host);
|
|
348
|
+
}
|
|
349
|
+
const fetchInit = {
|
|
193
350
|
...init,
|
|
351
|
+
headers,
|
|
194
352
|
signal: controller.signal,
|
|
195
353
|
redirect: "manual"
|
|
196
|
-
}
|
|
354
|
+
};
|
|
355
|
+
if (address) {
|
|
356
|
+
fetchInit.tls = { serverName: current.url.hostname };
|
|
357
|
+
}
|
|
358
|
+
const response = await fetch(fetchUrl, fetchInit);
|
|
197
359
|
if (response.status >= 300 && response.status < 400) {
|
|
198
360
|
const location = response.headers.get("location");
|
|
199
361
|
if (!location || redirectCount >= maxRedirects) {
|
|
200
362
|
return response;
|
|
201
363
|
}
|
|
202
|
-
|
|
364
|
+
current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
|
|
203
365
|
redirectCount += 1;
|
|
204
366
|
continue;
|
|
205
367
|
}
|
|
@@ -264,28 +426,49 @@ async function applyBypassToTransaction(transaction, bypass) {
|
|
|
264
426
|
bypass ? "true" : "false"
|
|
265
427
|
]);
|
|
266
428
|
}
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
return
|
|
429
|
+
function stringifyBypassIdentifier(identifier) {
|
|
430
|
+
if (typeof identifier === "number") {
|
|
431
|
+
return String(identifier);
|
|
270
432
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
433
|
+
return identifier.trim();
|
|
434
|
+
}
|
|
435
|
+
async function applyIdentifierToTransaction(transaction, identifier) {
|
|
436
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_identifier', $1, true)`, [
|
|
437
|
+
stringifyBypassIdentifier(identifier)
|
|
438
|
+
]);
|
|
439
|
+
}
|
|
440
|
+
function assertBypassIdentifier(identifier) {
|
|
441
|
+
if (typeof identifier === "number") {
|
|
442
|
+
if (!Number.isInteger(identifier) || identifier <= 0) {
|
|
443
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
278
444
|
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (typeof identifier === "string" && identifier.trim() !== "") {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
451
|
+
}
|
|
452
|
+
async function runWithScopedTenantTransaction(apply, callback) {
|
|
453
|
+
if (!isRlsTenancy()) {
|
|
454
|
+
return await callback();
|
|
279
455
|
}
|
|
280
456
|
const pool = getDefaultDatabasePool();
|
|
281
457
|
if (typeof pool.begin !== "function") {
|
|
282
458
|
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
283
459
|
}
|
|
284
460
|
return await pool.begin(async (transaction) => {
|
|
285
|
-
await
|
|
461
|
+
await apply(transaction);
|
|
286
462
|
return await runWithDatabaseConnection(transaction, callback);
|
|
287
463
|
});
|
|
288
464
|
}
|
|
465
|
+
async function runWithMigrationBypass(callback) {
|
|
466
|
+
return await runWithScopedTenantTransaction((transaction) => applyBypassToTransaction(transaction, true), callback);
|
|
467
|
+
}
|
|
468
|
+
async function runWithMigrationBypassForIdentifier(identifier, callback) {
|
|
469
|
+
assertBypassIdentifier(identifier);
|
|
470
|
+
return await runWithScopedTenantTransaction((transaction) => applyIdentifierToTransaction(transaction, identifier), callback);
|
|
471
|
+
}
|
|
289
472
|
|
|
290
473
|
// ../../src/core/audit/siemFormatter.ts
|
|
291
474
|
function formatSiemAuditEvent(input) {
|
|
@@ -18,6 +18,7 @@ function createAsyncContextStore(key) {
|
|
|
18
18
|
|
|
19
19
|
// ../../src/core/auth/authContext.ts
|
|
20
20
|
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
21
|
+
var credentialSourceContext = createAsyncContextStore("@getstrata/credentialSource");
|
|
21
22
|
function currentAuthUser() {
|
|
22
23
|
return authContext.getStore() ?? null;
|
|
23
24
|
}
|