@camstack/addon-auth 1.1.4 → 1.1.6
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/{dist-JUeAGU3c.mjs → dist-BlBNW5nA.mjs} +69 -1
- package/dist/{dist-BLBS0d1z.js → dist-CS-GzG-v.js} +80 -0
- package/dist/magic-link/auth-magic-link.addon.js +1 -1
- package/dist/magic-link/auth-magic-link.addon.mjs +1 -1
- package/dist/oidc/auth-oidc.addon.js +1 -1
- package/dist/oidc/auth-oidc.addon.mjs +1 -1
- package/dist/webauthn/_stub.js +121 -9
- package/dist/webauthn/{_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-Bg_T1-iY.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-Du23U8IZ.mjs} +2 -2
- package/dist/webauthn/auth-webauthn.addon.js +281 -55
- package/dist/webauthn/auth-webauthn.addon.mjs +281 -55
- package/dist/webauthn/{hostInit-Boe91Eeg.mjs → hostInit-Cq4HnxlU.mjs} +2 -2
- package/dist/webauthn/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-CS-GzG-v.js");
|
|
7
7
|
let stream = require("stream");
|
|
8
8
|
stream = require_chunk.__toESM(stream, 1);
|
|
9
9
|
let http = require("http");
|
|
@@ -12123,6 +12123,36 @@ async function verifyAuthenticationResponse(options) {
|
|
|
12123
12123
|
* auth like Touch ID), `usb`, `nfc`, `ble`.
|
|
12124
12124
|
*/
|
|
12125
12125
|
var COLLECTION = "user_passkeys";
|
|
12126
|
+
/**
|
|
12127
|
+
* Per-user passkey preferences — one row per userId, keyed upserts via
|
|
12128
|
+
* the settings-store `set` surface. Today a single flag: whether the
|
|
12129
|
+
* user OPTED IN to their passkey doubling as a mandatory second factor
|
|
12130
|
+
* after a password login (default OFF — enrolling a passkey only
|
|
12131
|
+
* enables passkey-first sign-in).
|
|
12132
|
+
*/
|
|
12133
|
+
var PREF_COLLECTION = "user_passkey_prefs";
|
|
12134
|
+
var PREF_COLUMNS = [
|
|
12135
|
+
{
|
|
12136
|
+
name: "userId",
|
|
12137
|
+
type: "TEXT",
|
|
12138
|
+
primaryKey: true,
|
|
12139
|
+
notNull: true
|
|
12140
|
+
},
|
|
12141
|
+
{
|
|
12142
|
+
name: "secondFactorEnabled",
|
|
12143
|
+
type: "BOOLEAN",
|
|
12144
|
+
notNull: true
|
|
12145
|
+
},
|
|
12146
|
+
{
|
|
12147
|
+
name: "updatedAt",
|
|
12148
|
+
type: "INTEGER",
|
|
12149
|
+
notNull: true
|
|
12150
|
+
}
|
|
12151
|
+
];
|
|
12152
|
+
var StoredPreferenceSchema = require_dist.object({
|
|
12153
|
+
secondFactorEnabled: require_dist.boolean(),
|
|
12154
|
+
updatedAt: require_dist.number()
|
|
12155
|
+
});
|
|
12126
12156
|
var COLUMNS = [
|
|
12127
12157
|
{
|
|
12128
12158
|
name: "credentialId",
|
|
@@ -12173,6 +12203,10 @@ async function declarePasskeySchema(store) {
|
|
|
12173
12203
|
columns: ["userId"]
|
|
12174
12204
|
}]
|
|
12175
12205
|
});
|
|
12206
|
+
await store.declareCollection.mutate({
|
|
12207
|
+
collection: PREF_COLLECTION,
|
|
12208
|
+
columns: PREF_COLUMNS
|
|
12209
|
+
});
|
|
12176
12210
|
}
|
|
12177
12211
|
function parseRow(data) {
|
|
12178
12212
|
const credentialId = data["credentialId"];
|
|
@@ -12248,6 +12282,31 @@ var PasskeyStore = class {
|
|
|
12248
12282
|
key: credentialId
|
|
12249
12283
|
});
|
|
12250
12284
|
}
|
|
12285
|
+
/**
|
|
12286
|
+
* Whether `userId` opted in to the passkey-as-second-factor gate.
|
|
12287
|
+
* Fail-safe: a missing or malformed row reads as `false` — the
|
|
12288
|
+
* password login flow must never demand a factor the user did not
|
|
12289
|
+
* explicitly enable.
|
|
12290
|
+
*/
|
|
12291
|
+
async getSecondFactorPreference(userId) {
|
|
12292
|
+
const raw = await this.store.get.query({
|
|
12293
|
+
collection: PREF_COLLECTION,
|
|
12294
|
+
key: userId
|
|
12295
|
+
});
|
|
12296
|
+
const parsed = StoredPreferenceSchema.safeParse(raw);
|
|
12297
|
+
return parsed.success ? parsed.data.secondFactorEnabled : false;
|
|
12298
|
+
}
|
|
12299
|
+
async setSecondFactorPreference(userId, enabled) {
|
|
12300
|
+
const value = {
|
|
12301
|
+
secondFactorEnabled: enabled,
|
|
12302
|
+
updatedAt: Date.now()
|
|
12303
|
+
};
|
|
12304
|
+
await this.store.set.mutate({
|
|
12305
|
+
collection: PREF_COLLECTION,
|
|
12306
|
+
key: userId,
|
|
12307
|
+
value
|
|
12308
|
+
});
|
|
12309
|
+
}
|
|
12251
12310
|
};
|
|
12252
12311
|
//#endregion
|
|
12253
12312
|
//#region src/webauthn/widget-catalog.ts
|
|
@@ -12257,61 +12316,97 @@ var AUTH_WEBAUTHN_REMOTE_NAME = "addon_auth_webauthn_widgets";
|
|
|
12257
12316
|
var AUTH_WEBAUTHN_BUNDLE = "remoteEntry.js";
|
|
12258
12317
|
/** Manifest addon id — the public bundle namespace + login-method addonId. */
|
|
12259
12318
|
var AUTH_WEBAUTHN_ADDON_ID = "auth-webauthn";
|
|
12260
|
-
var authWebauthnWidgets = [
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12287
|
-
|
|
12288
|
-
tab: "dashboard",
|
|
12289
|
-
label: "Passkey login",
|
|
12290
|
-
kind: "remote",
|
|
12291
|
-
remote: {
|
|
12292
|
-
remoteName: AUTH_WEBAUTHN_REMOTE_NAME,
|
|
12293
|
-
exposedModule: "./widgets",
|
|
12294
|
-
componentKey: "passkey-login"
|
|
12319
|
+
var authWebauthnWidgets = [
|
|
12320
|
+
{
|
|
12321
|
+
tab: "dashboard",
|
|
12322
|
+
label: "Passkeys",
|
|
12323
|
+
kind: "remote",
|
|
12324
|
+
remote: {
|
|
12325
|
+
remoteName: AUTH_WEBAUTHN_REMOTE_NAME,
|
|
12326
|
+
exposedModule: "./widgets",
|
|
12327
|
+
componentKey: "passkey-enrollment"
|
|
12328
|
+
},
|
|
12329
|
+
stableId: "passkey-enrollment",
|
|
12330
|
+
description: "Enroll and manage passkeys (WebAuthn) for your account.",
|
|
12331
|
+
icon: "fingerprint",
|
|
12332
|
+
bundle: AUTH_WEBAUTHN_BUNDLE,
|
|
12333
|
+
hosts: ["dashboard"],
|
|
12334
|
+
requires: {
|
|
12335
|
+
deviceContext: false,
|
|
12336
|
+
integrationContext: false
|
|
12337
|
+
},
|
|
12338
|
+
defaultSize: "lg",
|
|
12339
|
+
allowedSizes: [
|
|
12340
|
+
"md",
|
|
12341
|
+
"lg",
|
|
12342
|
+
"xl"
|
|
12343
|
+
],
|
|
12344
|
+
defaultColumns: 12,
|
|
12345
|
+
defaultRows: 3,
|
|
12346
|
+
preAuth: false
|
|
12295
12347
|
},
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12348
|
+
{
|
|
12349
|
+
tab: "dashboard",
|
|
12350
|
+
label: "Passkey login",
|
|
12351
|
+
kind: "remote",
|
|
12352
|
+
remote: {
|
|
12353
|
+
remoteName: AUTH_WEBAUTHN_REMOTE_NAME,
|
|
12354
|
+
exposedModule: "./widgets",
|
|
12355
|
+
componentKey: "passkey-login"
|
|
12356
|
+
},
|
|
12357
|
+
stableId: "passkey-login",
|
|
12358
|
+
description: "Login-page passkey second-factor ceremony.",
|
|
12359
|
+
icon: "fingerprint",
|
|
12360
|
+
bundle: AUTH_WEBAUTHN_BUNDLE,
|
|
12361
|
+
hosts: ["dashboard"],
|
|
12362
|
+
requires: {
|
|
12363
|
+
deviceContext: false,
|
|
12364
|
+
integrationContext: false
|
|
12365
|
+
},
|
|
12366
|
+
defaultSize: "sm",
|
|
12367
|
+
allowedSizes: ["sm"],
|
|
12368
|
+
defaultColumns: 4,
|
|
12369
|
+
defaultRows: 1,
|
|
12370
|
+
preAuth: true
|
|
12304
12371
|
},
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12372
|
+
{
|
|
12373
|
+
tab: "dashboard",
|
|
12374
|
+
label: "Passkey sign-in",
|
|
12375
|
+
kind: "remote",
|
|
12376
|
+
remote: {
|
|
12377
|
+
remoteName: AUTH_WEBAUTHN_REMOTE_NAME,
|
|
12378
|
+
exposedModule: "./widgets",
|
|
12379
|
+
componentKey: "passkey-direct-login"
|
|
12380
|
+
},
|
|
12381
|
+
stableId: "passkey-direct-login",
|
|
12382
|
+
description: "Login-page usernameless (discoverable-credential) passkey sign-in.",
|
|
12383
|
+
icon: "fingerprint",
|
|
12384
|
+
bundle: AUTH_WEBAUTHN_BUNDLE,
|
|
12385
|
+
hosts: ["dashboard"],
|
|
12386
|
+
requires: {
|
|
12387
|
+
deviceContext: false,
|
|
12388
|
+
integrationContext: false
|
|
12389
|
+
},
|
|
12390
|
+
defaultSize: "sm",
|
|
12391
|
+
allowedSizes: ["sm"],
|
|
12392
|
+
defaultColumns: 4,
|
|
12393
|
+
defaultRows: 1,
|
|
12394
|
+
preAuth: true
|
|
12395
|
+
}
|
|
12396
|
+
];
|
|
12311
12397
|
/**
|
|
12312
|
-
* Login-method
|
|
12313
|
-
*
|
|
12314
|
-
*
|
|
12398
|
+
* Login-method contributions — aggregated by the public
|
|
12399
|
+
* `auth.listLoginMethods` procedure, which stamps a public `bundleUrl`
|
|
12400
|
+
* from `addonId` + `bundle`.
|
|
12401
|
+
*
|
|
12402
|
+
* - `passkey-login` (second-factor) — the post-password 2FA ceremony,
|
|
12403
|
+
* mounted only when the challenged user has the `passkey` factor.
|
|
12404
|
+
* - `passkey-direct-login` (primary) — usernameless sign-in via the
|
|
12405
|
+
* standard WebAuthn discoverable-credentials flow. Contributed
|
|
12406
|
+
* unconditionally while the addon is registered: the browser's own
|
|
12407
|
+
* passkey picker is the honest gate (it simply offers nothing when
|
|
12408
|
+
* no resident credential matches the RP) — probing the server for
|
|
12409
|
+
* "does anyone have a passkey" pre-auth would leak enrollment state.
|
|
12315
12410
|
*/
|
|
12316
12411
|
var authWebauthnLoginMethods = [{
|
|
12317
12412
|
kind: "widget",
|
|
@@ -12324,6 +12419,17 @@ var authWebauthnLoginMethods = [{
|
|
|
12324
12419
|
componentKey: "passkey-login"
|
|
12325
12420
|
},
|
|
12326
12421
|
stage: "second-factor"
|
|
12422
|
+
}, {
|
|
12423
|
+
kind: "widget",
|
|
12424
|
+
id: `${AUTH_WEBAUTHN_ADDON_ID}/passkey-direct-login`,
|
|
12425
|
+
addonId: AUTH_WEBAUTHN_ADDON_ID,
|
|
12426
|
+
bundle: AUTH_WEBAUTHN_BUNDLE,
|
|
12427
|
+
remote: {
|
|
12428
|
+
remoteName: AUTH_WEBAUTHN_REMOTE_NAME,
|
|
12429
|
+
exposedModule: "./widgets",
|
|
12430
|
+
componentKey: "passkey-direct-login"
|
|
12431
|
+
},
|
|
12432
|
+
stage: "primary"
|
|
12327
12433
|
}];
|
|
12328
12434
|
//#endregion
|
|
12329
12435
|
//#region src/webauthn/auth-webauthn.addon.ts
|
|
@@ -12388,8 +12494,12 @@ var AuthWebauthnAddon = class extends require_dist.BaseAddon {
|
|
|
12388
12494
|
finishRegistration: async ({ userId, response, label }) => this.finishRegistration(userId, response, label),
|
|
12389
12495
|
beginAuthentication: async ({ userId }) => this.beginAuthentication(userId),
|
|
12390
12496
|
finishAuthentication: async ({ userId, response }) => this.finishAuthentication(userId, response),
|
|
12497
|
+
beginDiscoverableAuthentication: async () => this.beginDiscoverableAuthentication(),
|
|
12498
|
+
finishDiscoverableAuthentication: async ({ response }) => this.finishDiscoverableAuthentication(response),
|
|
12391
12499
|
listPasskeys: async ({ userId }) => this.listPasskeys(userId),
|
|
12392
|
-
removePasskey: async ({ userId, credentialId }) => this.removePasskey(userId, credentialId)
|
|
12500
|
+
removePasskey: async ({ userId, credentialId }) => this.removePasskey(userId, credentialId),
|
|
12501
|
+
getSecondFactorPreference: async ({ userId }) => this.getSecondFactorPreference(userId),
|
|
12502
|
+
setSecondFactorPreference: async ({ userId, enabled }) => this.setSecondFactorPreference(userId, enabled)
|
|
12393
12503
|
};
|
|
12394
12504
|
const widgetsProvider = { listWidgets: async () => authWebauthnWidgets };
|
|
12395
12505
|
const loginMethodProvider = { getLoginMethods: async () => authWebauthnLoginMethods };
|
|
@@ -12505,9 +12615,12 @@ var AuthWebauthnAddon = class extends require_dist.BaseAddon {
|
|
|
12505
12615
|
if (this.config.origin && this.config.origin.trim()) return this.config.origin.trim().replace(/\/+$/, "");
|
|
12506
12616
|
return (process.env["CAMSTACK_PUBLIC_ORIGIN"] || "http://localhost:4443").replace(/\/+$/, "");
|
|
12507
12617
|
}
|
|
12618
|
+
challengeTtlMs() {
|
|
12619
|
+
return (this.config.challengeTtlSec || DEFAULT_CONFIG.challengeTtlSec) * 1e3;
|
|
12620
|
+
}
|
|
12508
12621
|
pruneExpiredChallenges() {
|
|
12509
12622
|
const now = Date.now();
|
|
12510
|
-
const ttlMs =
|
|
12623
|
+
const ttlMs = this.challengeTtlMs();
|
|
12511
12624
|
for (const [k, v] of this.pending.entries()) if (now - v.createdAt > ttlMs) this.pending.delete(k);
|
|
12512
12625
|
}
|
|
12513
12626
|
base64urlEncode(bytes) {
|
|
@@ -12633,6 +12746,105 @@ var AuthWebauthnAddon = class extends require_dist.BaseAddon {
|
|
|
12633
12746
|
await this.store.updateCounter(credentialId, verification.authenticationInfo.newCounter);
|
|
12634
12747
|
return { verified: true };
|
|
12635
12748
|
}
|
|
12749
|
+
/**
|
|
12750
|
+
* Usernameless (discoverable-credential) login — leg 1. Issues
|
|
12751
|
+
* assertion options with an EMPTY `allowCredentials` list so the
|
|
12752
|
+
* browser offers every resident passkey it holds for this RP, and
|
|
12753
|
+
* `userVerification: 'required'` because the passkey is the ONLY
|
|
12754
|
+
* factor (it replaces both username/password and the 2FA leg).
|
|
12755
|
+
* The challenge is stored server-side keyed by its own value and is
|
|
12756
|
+
* NOT bound to any user — `finishDiscoverableAuthentication` resolves
|
|
12757
|
+
* the owner from the asserted credential id.
|
|
12758
|
+
*/
|
|
12759
|
+
async beginDiscoverableAuthentication() {
|
|
12760
|
+
if (!this.store) throw new Error("passkey store unavailable");
|
|
12761
|
+
this.pruneExpiredChallenges();
|
|
12762
|
+
const options = await generateAuthenticationOptions({
|
|
12763
|
+
rpID: this.resolveRpID(),
|
|
12764
|
+
allowCredentials: [],
|
|
12765
|
+
userVerification: "required"
|
|
12766
|
+
});
|
|
12767
|
+
this.pending.set(options.challenge, {
|
|
12768
|
+
challenge: options.challenge,
|
|
12769
|
+
userId: "",
|
|
12770
|
+
kind: "discoverable-authentication",
|
|
12771
|
+
createdAt: Date.now()
|
|
12772
|
+
});
|
|
12773
|
+
return { optionsJSON: options };
|
|
12774
|
+
}
|
|
12775
|
+
/**
|
|
12776
|
+
* Usernameless login — leg 2. Resolves the credential by the
|
|
12777
|
+
* response's credential id, verifies the assertion against the stored
|
|
12778
|
+
* challenge + that credential's public key/counter, and returns the
|
|
12779
|
+
* OWNING userId. Fail-closed: every anomaly returns
|
|
12780
|
+
* `{ verified: false, userId: null }`.
|
|
12781
|
+
*
|
|
12782
|
+
* Replay protection: the pending challenge is consumed (deleted)
|
|
12783
|
+
* BEFORE verification, so an assertion can be spent exactly once.
|
|
12784
|
+
* The TTL is enforced here too — pruning alone only runs on begin
|
|
12785
|
+
* legs, which an attacker controls the timing of.
|
|
12786
|
+
*/
|
|
12787
|
+
async finishDiscoverableAuthentication(response) {
|
|
12788
|
+
if (!this.store) return {
|
|
12789
|
+
verified: false,
|
|
12790
|
+
userId: null
|
|
12791
|
+
};
|
|
12792
|
+
const challenge = this.decodeClientData(response)?.challenge;
|
|
12793
|
+
if (!challenge) return {
|
|
12794
|
+
verified: false,
|
|
12795
|
+
userId: null
|
|
12796
|
+
};
|
|
12797
|
+
const pending = this.pending.get(challenge);
|
|
12798
|
+
if (!pending || pending.kind !== "discoverable-authentication") return {
|
|
12799
|
+
verified: false,
|
|
12800
|
+
userId: null
|
|
12801
|
+
};
|
|
12802
|
+
this.pending.delete(challenge);
|
|
12803
|
+
if (Date.now() - pending.createdAt > this.challengeTtlMs()) return {
|
|
12804
|
+
verified: false,
|
|
12805
|
+
userId: null
|
|
12806
|
+
};
|
|
12807
|
+
const credentialId = typeof response["id"] === "string" ? response["id"] : "";
|
|
12808
|
+
const stored = await this.store.findByCredentialId(credentialId);
|
|
12809
|
+
if (!stored) return {
|
|
12810
|
+
verified: false,
|
|
12811
|
+
userId: null
|
|
12812
|
+
};
|
|
12813
|
+
let verification;
|
|
12814
|
+
try {
|
|
12815
|
+
verification = await verifyAuthenticationResponse({
|
|
12816
|
+
response,
|
|
12817
|
+
expectedChallenge: challenge,
|
|
12818
|
+
expectedRPID: this.resolveRpID(),
|
|
12819
|
+
expectedOrigin: this.resolveOrigin(),
|
|
12820
|
+
requireUserVerification: true,
|
|
12821
|
+
credential: {
|
|
12822
|
+
id: stored.credentialId,
|
|
12823
|
+
publicKey: this.base64urlDecode(stored.publicKey),
|
|
12824
|
+
counter: stored.counter,
|
|
12825
|
+
transports: stored.transports
|
|
12826
|
+
}
|
|
12827
|
+
});
|
|
12828
|
+
} catch (err) {
|
|
12829
|
+
this.ctx.logger.warn("Discoverable passkey verification threw", { meta: {
|
|
12830
|
+
credentialId,
|
|
12831
|
+
err: err instanceof Error ? err.message : String(err)
|
|
12832
|
+
} });
|
|
12833
|
+
return {
|
|
12834
|
+
verified: false,
|
|
12835
|
+
userId: null
|
|
12836
|
+
};
|
|
12837
|
+
}
|
|
12838
|
+
if (!verification.verified || !verification.authenticationInfo) return {
|
|
12839
|
+
verified: false,
|
|
12840
|
+
userId: null
|
|
12841
|
+
};
|
|
12842
|
+
await this.store.updateCounter(credentialId, verification.authenticationInfo.newCounter);
|
|
12843
|
+
return {
|
|
12844
|
+
verified: true,
|
|
12845
|
+
userId: stored.userId
|
|
12846
|
+
};
|
|
12847
|
+
}
|
|
12636
12848
|
async listPasskeys(userId) {
|
|
12637
12849
|
if (!this.store) return [];
|
|
12638
12850
|
return (await this.store.findByUserId(userId)).map((r) => ({
|
|
@@ -12643,6 +12855,20 @@ var AuthWebauthnAddon = class extends require_dist.BaseAddon {
|
|
|
12643
12855
|
transports: [...r.transports]
|
|
12644
12856
|
}));
|
|
12645
12857
|
}
|
|
12858
|
+
/**
|
|
12859
|
+
* Second-factor preference (opt-in, default OFF). Enrolling a passkey
|
|
12860
|
+
* only enables passkey-first sign-in; the password login flow demands
|
|
12861
|
+
* the passkey second leg ONLY when this flag is explicitly enabled.
|
|
12862
|
+
*/
|
|
12863
|
+
async getSecondFactorPreference(userId) {
|
|
12864
|
+
if (!this.store) return { enabled: false };
|
|
12865
|
+
return { enabled: await this.store.getSecondFactorPreference(userId) };
|
|
12866
|
+
}
|
|
12867
|
+
async setSecondFactorPreference(userId, enabled) {
|
|
12868
|
+
if (!this.store) throw new Error("passkey store unavailable");
|
|
12869
|
+
await this.store.setSecondFactorPreference(userId, enabled);
|
|
12870
|
+
return { success: true };
|
|
12871
|
+
}
|
|
12646
12872
|
async removePasskey(userId, credentialId) {
|
|
12647
12873
|
if (!this.store) throw new Error("passkey store unavailable");
|
|
12648
12874
|
const existing = await this.store.findByCredentialId(credentialId);
|