@open-mercato/enterprise 0.6.8-develop.6975.1.b21e19d444 → 0.6.8-develop.6977.1.5041da373e
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/modules/record_locks/widgets/notifications/IncomingChangesRenderer.js +3 -2
- package/dist/modules/record_locks/widgets/notifications/IncomingChangesRenderer.js.map +2 -2
- package/dist/modules/security/lib/providers/PasskeyProvider.js +39 -51
- package/dist/modules/security/lib/providers/PasskeyProvider.js.map +3 -3
- package/package.json +5 -5
- package/src/modules/record_locks/widgets/notifications/IncomingChangesRenderer.tsx +3 -2
- package/src/modules/security/lib/providers/PasskeyProvider.ts +50 -62
|
@@ -4,7 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { GitPullRequestArrow, Calendar } from "lucide-react";
|
|
5
5
|
import { cn } from "@open-mercato/shared/lib/utils";
|
|
6
6
|
import { formatRelativeTime } from "@open-mercato/shared/lib/time";
|
|
7
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
7
|
+
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
8
|
function parseRows(notification) {
|
|
9
9
|
const raw = notification.bodyVariables?.changedRowsJson;
|
|
10
10
|
if (!raw || typeof raw !== "string") return [];
|
|
@@ -18,6 +18,7 @@ function parseRows(notification) {
|
|
|
18
18
|
}
|
|
19
19
|
function IncomingChangesRenderer({ notification }) {
|
|
20
20
|
const t = useT();
|
|
21
|
+
const locale = useLocale();
|
|
21
22
|
const rows = React.useMemo(() => parseRows(notification), [notification]);
|
|
22
23
|
const isUnread = notification.status === "unread";
|
|
23
24
|
return /* @__PURE__ */ jsxs(
|
|
@@ -36,7 +37,7 @@ function IncomingChangesRenderer({ notification }) {
|
|
|
36
37
|
/* @__PURE__ */ jsx("h4", { className: cn("text-sm font-medium", isUnread && "font-semibold"), children: notification.title }),
|
|
37
38
|
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
38
39
|
/* @__PURE__ */ jsx(Calendar, { className: "h-3 w-3" }),
|
|
39
|
-
formatRelativeTime(notification.createdAt, { translate: t }) ?? ""
|
|
40
|
+
formatRelativeTime(notification.createdAt, { locale, translate: t }) ?? ""
|
|
40
41
|
] })
|
|
41
42
|
] }),
|
|
42
43
|
/* @__PURE__ */ jsx("div", { className: "mt-2 overflow-x-auto rounded border border-border/70", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/record_locks/widgets/notifications/IncomingChangesRenderer.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { GitPullRequestArrow, Calendar } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\n\ntype ChangeRow = {\n field: string\n incoming: string\n current: string\n}\n\nfunction parseRows(notification: NotificationRendererProps['notification']): ChangeRow[] {\n const raw = notification.bodyVariables?.changedRowsJson\n if (!raw || typeof raw !== 'string') return []\n try {\n const parsed = JSON.parse(raw)\n if (!Array.isArray(parsed)) return []\n return parsed\n .filter((row): row is ChangeRow => (\n row\n && typeof row === 'object'\n && typeof (row as ChangeRow).field === 'string'\n && typeof (row as ChangeRow).incoming === 'string'\n && typeof (row as ChangeRow).current === 'string'\n ))\n .slice(0, 12)\n } catch {\n return []\n }\n}\n\nexport function IncomingChangesRenderer({ notification }: NotificationRendererProps) {\n const t = useT()\n const rows = React.useMemo(() => parseRows(notification), [notification])\n const isUnread = notification.status === 'unread'\n\n return (\n <div\n className={cn(\n 'group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-status-info-border',\n isUnread && 'bg-status-info-bg',\n )}\n >\n {isUnread ? (\n <div className=\"absolute left-1.5 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full bg-primary\" />\n ) : null}\n\n <div className=\"flex gap-3\">\n <div className=\"mt-0.5 flex-shrink-0\">\n <div className=\"h-10 w-10 rounded-lg bg-status-info-bg flex items-center justify-center\">\n <GitPullRequestArrow className=\"h-5 w-5 text-status-info-icon\" />\n </div>\n </div>\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <h4 className={cn('text-sm font-medium', isUnread && 'font-semibold')}>\n {notification.title}\n </h4>\n <span className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n <Calendar className=\"h-3 w-3\" />\n {formatRelativeTime(notification.createdAt, { translate: t }) ?? ''}\n </span>\n </div>\n\n <div className=\"mt-2 overflow-x-auto rounded border border-border/70\">\n <table className=\"w-full text-xs\">\n <thead className=\"bg-muted/50\">\n <tr>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.field', 'Field')}</th>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.incoming_label', 'Incoming')}</th>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.current_label', 'Current')}</th>\n </tr>\n </thead>\n <tbody>\n {rows.length ? rows.map((row, index) => (\n <tr key={`${row.field}-${index}`} className=\"border-t border-border/70\">\n <td className=\"px-2 py-1.5 align-top text-foreground\">{row.field}</td>\n <td className=\"px-2 py-1.5 align-top text-muted-foreground\">{row.incoming}</td>\n <td className=\"px-2 py-1.5 align-top text-muted-foreground\">{row.current}</td>\n </tr>\n )) : (\n <tr>\n <td className=\"px-2 py-2 text-muted-foreground\" colSpan={3}>\n {t('record_locks.conflict.no_field_details', 'Field-level conflict details are unavailable for this record. Choose a resolution to continue.')}\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n </div>\n </div>\n </div>\n )\n}\n\nexport default IncomingChangesRenderer\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { GitPullRequestArrow, Calendar } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\n\ntype ChangeRow = {\n field: string\n incoming: string\n current: string\n}\n\nfunction parseRows(notification: NotificationRendererProps['notification']): ChangeRow[] {\n const raw = notification.bodyVariables?.changedRowsJson\n if (!raw || typeof raw !== 'string') return []\n try {\n const parsed = JSON.parse(raw)\n if (!Array.isArray(parsed)) return []\n return parsed\n .filter((row): row is ChangeRow => (\n row\n && typeof row === 'object'\n && typeof (row as ChangeRow).field === 'string'\n && typeof (row as ChangeRow).incoming === 'string'\n && typeof (row as ChangeRow).current === 'string'\n ))\n .slice(0, 12)\n } catch {\n return []\n }\n}\n\nexport function IncomingChangesRenderer({ notification }: NotificationRendererProps) {\n const t = useT()\n const locale = useLocale()\n const rows = React.useMemo(() => parseRows(notification), [notification])\n const isUnread = notification.status === 'unread'\n\n return (\n <div\n className={cn(\n 'group relative px-4 py-3 hover:bg-muted/50 transition-colors border-l-4 border-l-status-info-border',\n isUnread && 'bg-status-info-bg',\n )}\n >\n {isUnread ? (\n <div className=\"absolute left-1.5 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full bg-primary\" />\n ) : null}\n\n <div className=\"flex gap-3\">\n <div className=\"mt-0.5 flex-shrink-0\">\n <div className=\"h-10 w-10 rounded-lg bg-status-info-bg flex items-center justify-center\">\n <GitPullRequestArrow className=\"h-5 w-5 text-status-info-icon\" />\n </div>\n </div>\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <h4 className={cn('text-sm font-medium', isUnread && 'font-semibold')}>\n {notification.title}\n </h4>\n <span className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n <Calendar className=\"h-3 w-3\" />\n {formatRelativeTime(notification.createdAt, { locale, translate: t }) ?? ''}\n </span>\n </div>\n\n <div className=\"mt-2 overflow-x-auto rounded border border-border/70\">\n <table className=\"w-full text-xs\">\n <thead className=\"bg-muted/50\">\n <tr>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.field', 'Field')}</th>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.incoming_label', 'Incoming')}</th>\n <th className=\"px-2 py-1.5 text-left font-medium\">{t('record_locks.conflict.current_label', 'Current')}</th>\n </tr>\n </thead>\n <tbody>\n {rows.length ? rows.map((row, index) => (\n <tr key={`${row.field}-${index}`} className=\"border-t border-border/70\">\n <td className=\"px-2 py-1.5 align-top text-foreground\">{row.field}</td>\n <td className=\"px-2 py-1.5 align-top text-muted-foreground\">{row.incoming}</td>\n <td className=\"px-2 py-1.5 align-top text-muted-foreground\">{row.current}</td>\n </tr>\n )) : (\n <tr>\n <td className=\"px-2 py-2 text-muted-foreground\" colSpan={3}>\n {t('record_locks.conflict.no_field_details', 'Field-level conflict details are unavailable for this record. Choose a resolution to continue.')}\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n </div>\n </div>\n </div>\n )\n}\n\nexport default IncomingChangesRenderer\n"],
|
|
5
|
+
"mappings": ";AAiDQ,cAeI,YAfJ;AA/CR,YAAY,WAAW;AACvB,SAAS,qBAAqB,gBAAgB;AAC9C,SAAS,UAAU;AACnB,SAAS,0BAA0B;AACnC,SAAS,WAAW,YAAY;AAShC,SAAS,UAAU,cAAsE;AACvF,QAAM,MAAM,aAAa,eAAe;AACxC,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AACpC,WAAO,OACJ,OAAO,CAAC,QACP,OACG,OAAO,QAAQ,YACf,OAAQ,IAAkB,UAAU,YACpC,OAAQ,IAAkB,aAAa,YACvC,OAAQ,IAAkB,YAAY,QAC1C,EACA,MAAM,GAAG,EAAE;AAAA,EAChB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,wBAAwB,EAAE,aAAa,GAA8B;AACnF,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,OAAO,MAAM,QAAQ,MAAM,UAAU,YAAY,GAAG,CAAC,YAAY,CAAC;AACxE,QAAM,WAAW,aAAa,WAAW;AAEzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,YAAY;AAAA,MACd;AAAA,MAEC;AAAA,mBACC,oBAAC,SAAI,WAAU,8EAA6E,IAC1F;AAAA,QAEJ,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAI,WAAU,wBACb,8BAAC,SAAI,WAAU,2EACb,8BAAC,uBAAoB,WAAU,iCAAgC,GACjE,GACF;AAAA,UAEA,qBAAC,SAAI,WAAU,kBACb;AAAA,iCAAC,SAAI,WAAU,0CACb;AAAA,kCAAC,QAAG,WAAW,GAAG,uBAAuB,YAAY,eAAe,GACjE,uBAAa,OAChB;AAAA,cACA,qBAAC,UAAK,WAAU,yDACd;AAAA,oCAAC,YAAS,WAAU,WAAU;AAAA,gBAC7B,mBAAmB,aAAa,WAAW,EAAE,QAAQ,WAAW,EAAE,CAAC,KAAK;AAAA,iBAC3E;AAAA,eACF;AAAA,YAEA,oBAAC,SAAI,WAAU,wDACb,+BAAC,WAAM,WAAU,kBACf;AAAA,kCAAC,WAAM,WAAU,eACf,+BAAC,QACC;AAAA,oCAAC,QAAG,WAAU,qCAAqC,YAAE,+BAA+B,OAAO,GAAE;AAAA,gBAC7F,oBAAC,QAAG,WAAU,qCAAqC,YAAE,wCAAwC,UAAU,GAAE;AAAA,gBACzG,oBAAC,QAAG,WAAU,qCAAqC,YAAE,uCAAuC,SAAS,GAAE;AAAA,iBACzG,GACF;AAAA,cACA,oBAAC,WACE,eAAK,SAAS,KAAK,IAAI,CAAC,KAAK,UAC5B,qBAAC,QAAiC,WAAU,6BAC1C;AAAA,oCAAC,QAAG,WAAU,yCAAyC,cAAI,OAAM;AAAA,gBACjE,oBAAC,QAAG,WAAU,+CAA+C,cAAI,UAAS;AAAA,gBAC1E,oBAAC,QAAG,WAAU,+CAA+C,cAAI,SAAQ;AAAA,mBAHlE,GAAG,IAAI,KAAK,IAAI,KAAK,EAI9B,CACD,IACC,oBAAC,QACC,8BAAC,QAAG,WAAU,mCAAkC,SAAS,GACtD,YAAE,0CAA0C,gGAAgG,GAC/I,GACF,GAEJ;AAAA,eACF,GACF;AAAA,aACF;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAO,kCAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
verifyAuthenticationResponse,
|
|
6
6
|
verifyRegistrationResponse
|
|
7
7
|
} from "@simplewebauthn/server";
|
|
8
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import {
|
|
10
11
|
readSecurityModuleConfig,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
resolveSecurityModuleConfigForRequest
|
|
13
14
|
} from "../security-config.js";
|
|
14
15
|
const SETUP_TOKEN_VERSION = "v1";
|
|
16
|
+
const logger = createLogger("security").child({ component: "passkey-provider" });
|
|
15
17
|
const setupPayloadSchema = z.object({
|
|
16
18
|
label: z.string().min(1).max(100).optional(),
|
|
17
19
|
userName: z.string().min(1).max(255).optional(),
|
|
@@ -30,15 +32,9 @@ const setupConfirmationPayloadSchema = z.union([
|
|
|
30
32
|
label: z.string().min(1).max(100).optional()
|
|
31
33
|
})
|
|
32
34
|
]);
|
|
33
|
-
const verifyPayloadSchema = z.
|
|
34
|
-
z.
|
|
35
|
-
|
|
36
|
-
}),
|
|
37
|
-
z.object({
|
|
38
|
-
credentialId: z.string().min(1),
|
|
39
|
-
challenge: z.string().min(1)
|
|
40
|
-
})
|
|
41
|
-
]);
|
|
35
|
+
const verifyPayloadSchema = z.object({
|
|
36
|
+
response: z.record(z.string(), z.unknown())
|
|
37
|
+
});
|
|
42
38
|
const setupTokenSchema = z.object({
|
|
43
39
|
version: z.string().min(1),
|
|
44
40
|
userId: z.string().min(1),
|
|
@@ -198,59 +194,51 @@ class PasskeyProvider {
|
|
|
198
194
|
};
|
|
199
195
|
}
|
|
200
196
|
async verify(userId, method, payload, context, runtimeContext) {
|
|
201
|
-
const parsed = verifyPayloadSchema.
|
|
197
|
+
const parsed = verifyPayloadSchema.safeParse(payload);
|
|
198
|
+
if (!parsed.success) return false;
|
|
202
199
|
if (method.userId !== userId) return false;
|
|
203
200
|
const parsedContext = verifyContextSchema.safeParse(context);
|
|
201
|
+
if (!parsedContext.success) return false;
|
|
204
202
|
const securityConfig = this.resolveSecurityConfig(runtimeContext);
|
|
205
|
-
if (!parsedContext.success) {
|
|
206
|
-
if ("credentialId" in parsed) {
|
|
207
|
-
const metadataCredentialId2 = method.providerMetadata?.credentialId;
|
|
208
|
-
return typeof metadataCredentialId2 === "string" && metadataCredentialId2 === parsed.credentialId;
|
|
209
|
-
}
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
203
|
const pending = parsedContext.data.challenge;
|
|
213
204
|
if (Date.now() - pending.createdAt > securityConfig.webauthn.challengeTtlMs) {
|
|
214
205
|
return false;
|
|
215
206
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
207
|
+
const metadata = method.providerMetadata ?? {};
|
|
208
|
+
const credentialId = typeof metadata.credentialId === "string" ? metadata.credentialId : null;
|
|
209
|
+
const credentialPublicKey = typeof metadata.credentialPublicKey === "string" ? metadata.credentialPublicKey : typeof metadata.publicKey === "string" ? metadata.publicKey : null;
|
|
210
|
+
const counter = typeof metadata.counter === "number" && Number.isFinite(metadata.counter) ? metadata.counter : 0;
|
|
211
|
+
if (!credentialId || !credentialPublicKey) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const verification = await verifyAuthenticationResponse({
|
|
215
|
+
response: parsed.data.response,
|
|
216
|
+
expectedChallenge: pending.challenge,
|
|
217
|
+
expectedOrigin: this.getExpectedOrigins(securityConfig),
|
|
218
|
+
expectedRPID: this.getRpId(securityConfig),
|
|
219
|
+
requireUserVerification: false,
|
|
220
|
+
credential: {
|
|
221
|
+
id: credentialId,
|
|
222
|
+
publicKey: this.base64UrlToBytes(credentialPublicKey),
|
|
223
|
+
counter,
|
|
224
|
+
transports: this.normalizeTransports(metadata.transports)
|
|
223
225
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
requireUserVerification: false,
|
|
230
|
-
credential: {
|
|
231
|
-
id: credentialId,
|
|
232
|
-
publicKey: this.base64UrlToBytes(credentialPublicKey),
|
|
233
|
-
counter,
|
|
234
|
-
transports: this.normalizeTransports(metadata.transports)
|
|
235
|
-
}
|
|
226
|
+
}).catch((error) => {
|
|
227
|
+
logger.warn("Passkey assertion could not be verified", {
|
|
228
|
+
methodId: method.id,
|
|
229
|
+
rpId: this.getRpId(securityConfig),
|
|
230
|
+
err: error
|
|
236
231
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const nextCounter = verification.authenticationInfo?.newCounter;
|
|
241
|
-
if (typeof nextCounter === "number" && Number.isFinite(nextCounter)) {
|
|
242
|
-
const providerMetadata = method.providerMetadata ?? {};
|
|
243
|
-
providerMetadata.counter = nextCounter;
|
|
244
|
-
method.providerMetadata = providerMetadata;
|
|
245
|
-
}
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
const metadataCredentialId = method.providerMetadata?.credentialId;
|
|
249
|
-
if (typeof metadataCredentialId !== "string" || metadataCredentialId !== parsed.credentialId) {
|
|
232
|
+
return null;
|
|
233
|
+
});
|
|
234
|
+
if (!verification?.verified) {
|
|
250
235
|
return false;
|
|
251
236
|
}
|
|
252
|
-
|
|
253
|
-
|
|
237
|
+
const nextCounter = verification.authenticationInfo?.newCounter;
|
|
238
|
+
if (typeof nextCounter === "number" && Number.isFinite(nextCounter)) {
|
|
239
|
+
const providerMetadata = method.providerMetadata ?? {};
|
|
240
|
+
providerMetadata.counter = nextCounter;
|
|
241
|
+
method.providerMetadata = providerMetadata;
|
|
254
242
|
}
|
|
255
243
|
return true;
|
|
256
244
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/security/lib/providers/PasskeyProvider.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport {\n generateAuthenticationOptions,\n generateRegistrationOptions,\n verifyAuthenticationResponse,\n verifyRegistrationResponse,\n} from '@simplewebauthn/server'\nimport type { AuthenticatorTransportFuture } from '@simplewebauthn/types'\nimport { z } from 'zod'\nimport type {\n MfaMethodRecord,\n MfaProviderConfirmResult,\n MfaProviderInterface,\n MfaProviderRuntimeContext,\n MfaProviderUser,\n MfaVerifyContext,\n} from '../mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../security-config'\nimport {\n readSecurityModuleConfig,\n readSecuritySetupTokenSecret,\n resolveSecurityModuleConfigForRequest,\n} from '../security-config'\n\nconst SETUP_TOKEN_VERSION = 'v1'\n\nconst setupPayloadSchema = z.object({\n label: z.string().min(1).max(100).optional(),\n userName: z.string().min(1).max(255).optional(),\n authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(),\n})\n\nconst setupConfirmationPayloadSchema = z.union([\n z.object({\n response: z.record(z.string(), z.unknown()),\n label: z.string().min(1).max(100).optional(),\n }),\n z.object({\n credentialId: z.string().min(1),\n publicKey: z.string().min(1),\n challenge: z.string().min(1),\n transports: z.array(z.string().min(1)).optional(),\n label: z.string().min(1).max(100).optional(),\n }),\n])\n\nconst verifyPayloadSchema = z.union([\n z.object({\n response: z.record(z.string(), z.unknown()),\n }),\n z.object({\n credentialId: z.string().min(1),\n challenge: z.string().min(1),\n }),\n])\n\nconst setupTokenSchema = z.object({\n version: z.string().min(1),\n userId: z.string().min(1),\n challenge: z.string().min(1),\n label: z.string().min(1).max(100).optional(),\n createdAt: z.number().int().nonnegative(),\n nonce: z.string().min(1),\n})\n\ntype PendingSetup = {\n version: string\n userId: string\n challenge: string\n label?: string\n createdAt: number\n nonce: string\n}\n\nconst verifyContextSchema = z.object({\n challenge: z.object({\n challenge: z.string().min(1),\n createdAt: z.number().int().nonnegative(),\n }),\n})\n\nconst AUTHENTICATOR_TRANSPORTS: AuthenticatorTransportFuture[] = [\n 'ble',\n 'cable',\n 'hybrid',\n 'internal',\n 'nfc',\n 'smart-card',\n 'usb',\n]\n\nexport class PasskeyProvider implements MfaProviderInterface {\n readonly type = 'passkey'\n readonly label = 'Passkey'\n readonly icon = 'Key'\n readonly allowMultiple = true\n readonly setupSchema = setupPayloadSchema\n readonly verifySchema = verifyPayloadSchema\n\n constructor(\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n private readonly setupTokenSecret: string = readSecuritySetupTokenSecret(),\n ) {}\n\n resolveSetupPayload(user: MfaProviderUser, payload: unknown): unknown {\n const parsed = setupPayloadSchema.parse(payload ?? {})\n const email = typeof user.email === 'string' ? user.email.trim() : ''\n\n return {\n ...parsed,\n ...(parsed.label || email.length === 0 ? {} : { label: email }),\n ...(parsed.userName || email.length === 0 ? {} : { userName: email }),\n }\n }\n\n async setup(\n userId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ setupId: string; clientData: Record<string, unknown> }> {\n const parsed = setupPayloadSchema.parse(payload ?? {})\n const userName = parsed.userName ?? parsed.label ?? userId\n const userDisplayName = parsed.label ?? parsed.userName ?? userId\n const securityConfig = this.resolveSecurityConfig(context)\n const options = await generateRegistrationOptions({\n rpName: this.getRpName(securityConfig),\n rpID: this.getRpId(securityConfig),\n userID: this.toWebAuthnUserId(userId),\n userName,\n userDisplayName,\n timeout: securityConfig.webauthn.setupTtlMs,\n attestationType: 'none',\n authenticatorSelection: {\n residentKey: 'required',\n userVerification: 'preferred',\n ...(parsed.authenticatorAttachment ? { authenticatorAttachment: parsed.authenticatorAttachment } : {}),\n },\n })\n\n const now = Date.now()\n const setupId = this.createSetupToken({\n version: SETUP_TOKEN_VERSION,\n userId,\n challenge: options.challenge,\n label: parsed.label,\n createdAt: now,\n nonce: randomBytes(16).toString('hex'),\n })\n\n return {\n setupId,\n clientData: options as unknown as Record<string, unknown>,\n }\n }\n\n async confirmSetup(\n userId: string,\n setupId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<MfaProviderConfirmResult> {\n const parsed = setupConfirmationPayloadSchema.parse(payload)\n const pending = this.readSetupToken(setupId)\n const securityConfig = this.resolveSecurityConfig(context)\n if (!pending || pending.version !== SETUP_TOKEN_VERSION || pending.userId !== userId) {\n throw new Error('Passkey setup session not found')\n }\n if (Date.now() - pending.createdAt > securityConfig.webauthn.setupTtlMs) {\n throw new Error('Passkey setup session expired')\n }\n\n if ('response' in parsed) {\n const verification = await verifyRegistrationResponse({\n response: parsed.response as never,\n expectedChallenge: pending.challenge,\n expectedOrigin: this.getExpectedOrigins(securityConfig),\n expectedRPID: this.getRpId(securityConfig),\n requireUserVerification: false,\n })\n\n if (!verification.verified || !verification.registrationInfo) {\n throw new Error('Passkey registration verification failed')\n }\n\n const registrationInfo = verification.registrationInfo as {\n credential?: {\n id: string\n publicKey: Uint8Array\n counter: number\n transports?: string[]\n }\n credentialID?: string\n credentialPublicKey?: Uint8Array\n counter?: number\n }\n\n const credentialId = registrationInfo.credential?.id ?? registrationInfo.credentialID\n const publicKeyBytes = registrationInfo.credential?.publicKey ?? registrationInfo.credentialPublicKey\n const counter = registrationInfo.credential?.counter ?? registrationInfo.counter ?? 0\n const transports = this.normalizeTransports(registrationInfo.credential?.transports)\n\n if (!credentialId || !publicKeyBytes) {\n throw new Error('Passkey registration did not return credential data')\n }\n\n return {\n metadata: {\n credentialId,\n credentialPublicKey: this.bytesToBase64Url(publicKeyBytes),\n counter,\n transports,\n label: parsed.label ?? pending.label ?? 'Passkey',\n },\n }\n }\n\n if (parsed.challenge !== pending.challenge) {\n throw new Error('Invalid passkey setup challenge')\n }\n\n return {\n metadata: {\n credentialId: parsed.credentialId,\n credentialPublicKey: parsed.publicKey,\n counter: 0,\n transports: parsed.transports ?? [],\n label: parsed.label ?? pending.label ?? 'Passkey',\n },\n }\n }\n\n async prepareChallenge(\n userId: string,\n method: MfaMethodRecord,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown>; verifyContext?: MfaVerifyContext }> {\n if (method.userId !== userId) {\n throw new Error('MFA method does not belong to user')\n }\n\n const metadata = method.providerMetadata ?? {}\n const credentialId = typeof metadata.credentialId === 'string' ? metadata.credentialId : null\n if (!credentialId) {\n throw new Error('Passkey credential is not configured')\n }\n\n const securityConfig = this.resolveSecurityConfig(context)\n const options = await generateAuthenticationOptions({\n rpID: this.getRpId(securityConfig),\n userVerification: 'preferred',\n timeout: securityConfig.webauthn.challengeTtlMs,\n allowCredentials: [{\n id: credentialId,\n transports: this.normalizeTransports(metadata.transports),\n }],\n })\n\n const now = Date.now()\n\n return {\n clientData: options as unknown as Record<string, unknown>,\n verifyContext: {\n challenge: {\n challenge: options.challenge,\n createdAt: now,\n },\n },\n }\n }\n\n async verify(\n userId: string,\n method: MfaMethodRecord,\n payload: unknown,\n context?: MfaVerifyContext,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean> {\n const parsed = verifyPayloadSchema.parse(payload)\n if (method.userId !== userId) return false\n\n const parsedContext = verifyContextSchema.safeParse(context)\n const securityConfig = this.resolveSecurityConfig(runtimeContext)\n if (!parsedContext.success) {\n if ('credentialId' in parsed) {\n const metadataCredentialId = method.providerMetadata?.credentialId\n return typeof metadataCredentialId === 'string' && metadataCredentialId === parsed.credentialId\n }\n return false\n }\n const pending = parsedContext.data.challenge\n if (Date.now() - pending.createdAt > securityConfig.webauthn.challengeTtlMs) {\n return false\n }\n\n if ('response' in parsed) {\n const metadata = method.providerMetadata ?? {}\n const credentialId = typeof metadata.credentialId === 'string' ? metadata.credentialId : null\n const credentialPublicKey = typeof metadata.credentialPublicKey === 'string'\n ? metadata.credentialPublicKey\n : typeof metadata.publicKey === 'string'\n ? metadata.publicKey\n : null\n const counter = typeof metadata.counter === 'number' && Number.isFinite(metadata.counter)\n ? metadata.counter\n : 0\n\n if (!credentialId || !credentialPublicKey) {\n return false\n }\n\n const verification = await verifyAuthenticationResponse({\n response: parsed.response as never,\n expectedChallenge: pending.challenge,\n expectedOrigin: this.getExpectedOrigins(securityConfig),\n expectedRPID: this.getRpId(securityConfig),\n requireUserVerification: false,\n credential: {\n id: credentialId,\n publicKey: this.base64UrlToBytes(credentialPublicKey),\n counter,\n transports: this.normalizeTransports(metadata.transports),\n },\n })\n\n if (!verification.verified) {\n return false\n }\n\n const nextCounter = verification.authenticationInfo?.newCounter\n if (typeof nextCounter === 'number' && Number.isFinite(nextCounter)) {\n const providerMetadata = method.providerMetadata ?? {}\n providerMetadata.counter = nextCounter\n method.providerMetadata = providerMetadata\n }\n\n return true\n }\n\n const metadataCredentialId = method.providerMetadata?.credentialId\n if (typeof metadataCredentialId !== 'string' || metadataCredentialId !== parsed.credentialId) {\n return false\n }\n if (pending.challenge !== parsed.challenge) {\n return false\n }\n\n return true\n }\n\n private normalizeTransports(raw: unknown): AuthenticatorTransportFuture[] {\n if (!Array.isArray(raw)) return []\n return raw.filter((value: unknown): value is AuthenticatorTransportFuture =>\n typeof value === 'string' && AUTHENTICATOR_TRANSPORTS.includes(value as AuthenticatorTransportFuture),\n )\n }\n\n private bytesToBase64Url(value: Uint8Array): string {\n return Buffer.from(value).toString('base64url')\n }\n\n private base64UrlToBytes(value: string) {\n return new Uint8Array(Buffer.from(value, 'base64url')).slice()\n }\n\n private createSetupToken(setup: PendingSetup): string {\n const encodedPayload = Buffer.from(JSON.stringify(setup), 'utf8').toString('base64url')\n const signature = this.signSetupPayload(encodedPayload)\n return `${encodedPayload}.${signature}`\n }\n\n private readSetupToken(token: string): PendingSetup | null {\n const [encodedPayload, signature, ...extra] = token.split('.')\n if (!encodedPayload || !signature || extra.length > 0) {\n return null\n }\n\n const expectedSignature = this.signSetupPayload(encodedPayload)\n const providedBuffer = Buffer.from(signature, 'base64url')\n const expectedBuffer = Buffer.from(expectedSignature, 'base64url')\n if (providedBuffer.length !== expectedBuffer.length || !timingSafeEqual(providedBuffer, expectedBuffer)) {\n return null\n }\n\n try {\n const raw = Buffer.from(encodedPayload, 'base64url').toString('utf8')\n const parsed = JSON.parse(raw)\n const result = setupTokenSchema.safeParse(parsed)\n return result.success ? result.data : null\n } catch {\n return null\n }\n }\n\n private signSetupPayload(encodedPayload: string): string {\n return createHmac('sha256', this.getSetupTokenSecret()).update(encodedPayload).digest('base64url')\n }\n\n private getSetupTokenSecret(): string {\n return this.setupTokenSecret\n }\n\n private getRpName(securityConfig: SecurityModuleConfig): string {\n return securityConfig.webauthn.rpName\n }\n\n private getRpId(securityConfig: SecurityModuleConfig): string {\n return securityConfig.webauthn.rpId\n }\n\n private getExpectedOrigins(securityConfig: SecurityModuleConfig): string[] {\n return [...securityConfig.webauthn.expectedOrigins]\n }\n\n private resolveSecurityConfig(context?: MfaProviderRuntimeContext): SecurityModuleConfig {\n return resolveSecurityModuleConfigForRequest(this.securityConfig, context?.request)\n }\n\n private toWebAuthnUserId(userId: string) {\n return new TextEncoder().encode(userId).slice()\n }\n}\n\nexport default PasskeyProvider\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,aAAa,uBAAuB;AACzD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,SAAS;AAUlB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,sBAAsB;AAE5B,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC3C,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC9C,yBAAyB,EAAE,KAAK,CAAC,YAAY,gBAAgB,CAAC,EAAE,SAAS;AAC3E,CAAC;AAED,MAAM,iCAAiC,EAAE,MAAM;AAAA,EAC7C,EAAE,OAAO;AAAA,IACP,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,IAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC7C,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC9B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC3B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC3B,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,SAAS;AAAA,IAChD,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,EAC7C,CAAC;AACH,CAAC;AAED,MAAM,sBAAsB,EAAE,
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport {\n generateAuthenticationOptions,\n generateRegistrationOptions,\n verifyAuthenticationResponse,\n verifyRegistrationResponse,\n} from '@simplewebauthn/server'\nimport type { AuthenticatorTransportFuture } from '@simplewebauthn/types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { z } from 'zod'\nimport type {\n MfaMethodRecord,\n MfaProviderConfirmResult,\n MfaProviderInterface,\n MfaProviderRuntimeContext,\n MfaProviderUser,\n MfaVerifyContext,\n} from '../mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../security-config'\nimport {\n readSecurityModuleConfig,\n readSecuritySetupTokenSecret,\n resolveSecurityModuleConfigForRequest,\n} from '../security-config'\n\nconst SETUP_TOKEN_VERSION = 'v1'\n\nconst logger = createLogger('security').child({ component: 'passkey-provider' })\n\nconst setupPayloadSchema = z.object({\n label: z.string().min(1).max(100).optional(),\n userName: z.string().min(1).max(255).optional(),\n authenticatorAttachment: z.enum(['platform', 'cross-platform']).optional(),\n})\n\nconst setupConfirmationPayloadSchema = z.union([\n z.object({\n response: z.record(z.string(), z.unknown()),\n label: z.string().min(1).max(100).optional(),\n }),\n z.object({\n credentialId: z.string().min(1),\n publicKey: z.string().min(1),\n challenge: z.string().min(1),\n transports: z.array(z.string().min(1)).optional(),\n label: z.string().min(1).max(100).optional(),\n }),\n])\n\nconst verifyPayloadSchema = z.object({\n response: z.record(z.string(), z.unknown()),\n})\n\nconst setupTokenSchema = z.object({\n version: z.string().min(1),\n userId: z.string().min(1),\n challenge: z.string().min(1),\n label: z.string().min(1).max(100).optional(),\n createdAt: z.number().int().nonnegative(),\n nonce: z.string().min(1),\n})\n\ntype PendingSetup = {\n version: string\n userId: string\n challenge: string\n label?: string\n createdAt: number\n nonce: string\n}\n\nconst verifyContextSchema = z.object({\n challenge: z.object({\n challenge: z.string().min(1),\n createdAt: z.number().int().nonnegative(),\n }),\n})\n\nconst AUTHENTICATOR_TRANSPORTS: AuthenticatorTransportFuture[] = [\n 'ble',\n 'cable',\n 'hybrid',\n 'internal',\n 'nfc',\n 'smart-card',\n 'usb',\n]\n\nexport class PasskeyProvider implements MfaProviderInterface {\n readonly type = 'passkey'\n readonly label = 'Passkey'\n readonly icon = 'Key'\n readonly allowMultiple = true\n readonly setupSchema = setupPayloadSchema\n readonly verifySchema = verifyPayloadSchema\n\n constructor(\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n private readonly setupTokenSecret: string = readSecuritySetupTokenSecret(),\n ) {}\n\n resolveSetupPayload(user: MfaProviderUser, payload: unknown): unknown {\n const parsed = setupPayloadSchema.parse(payload ?? {})\n const email = typeof user.email === 'string' ? user.email.trim() : ''\n\n return {\n ...parsed,\n ...(parsed.label || email.length === 0 ? {} : { label: email }),\n ...(parsed.userName || email.length === 0 ? {} : { userName: email }),\n }\n }\n\n async setup(\n userId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ setupId: string; clientData: Record<string, unknown> }> {\n const parsed = setupPayloadSchema.parse(payload ?? {})\n const userName = parsed.userName ?? parsed.label ?? userId\n const userDisplayName = parsed.label ?? parsed.userName ?? userId\n const securityConfig = this.resolveSecurityConfig(context)\n const options = await generateRegistrationOptions({\n rpName: this.getRpName(securityConfig),\n rpID: this.getRpId(securityConfig),\n userID: this.toWebAuthnUserId(userId),\n userName,\n userDisplayName,\n timeout: securityConfig.webauthn.setupTtlMs,\n attestationType: 'none',\n authenticatorSelection: {\n residentKey: 'required',\n userVerification: 'preferred',\n ...(parsed.authenticatorAttachment ? { authenticatorAttachment: parsed.authenticatorAttachment } : {}),\n },\n })\n\n const now = Date.now()\n const setupId = this.createSetupToken({\n version: SETUP_TOKEN_VERSION,\n userId,\n challenge: options.challenge,\n label: parsed.label,\n createdAt: now,\n nonce: randomBytes(16).toString('hex'),\n })\n\n return {\n setupId,\n clientData: options as unknown as Record<string, unknown>,\n }\n }\n\n async confirmSetup(\n userId: string,\n setupId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<MfaProviderConfirmResult> {\n const parsed = setupConfirmationPayloadSchema.parse(payload)\n const pending = this.readSetupToken(setupId)\n const securityConfig = this.resolveSecurityConfig(context)\n if (!pending || pending.version !== SETUP_TOKEN_VERSION || pending.userId !== userId) {\n throw new Error('Passkey setup session not found')\n }\n if (Date.now() - pending.createdAt > securityConfig.webauthn.setupTtlMs) {\n throw new Error('Passkey setup session expired')\n }\n\n if ('response' in parsed) {\n const verification = await verifyRegistrationResponse({\n response: parsed.response as never,\n expectedChallenge: pending.challenge,\n expectedOrigin: this.getExpectedOrigins(securityConfig),\n expectedRPID: this.getRpId(securityConfig),\n requireUserVerification: false,\n })\n\n if (!verification.verified || !verification.registrationInfo) {\n throw new Error('Passkey registration verification failed')\n }\n\n const registrationInfo = verification.registrationInfo as {\n credential?: {\n id: string\n publicKey: Uint8Array\n counter: number\n transports?: string[]\n }\n credentialID?: string\n credentialPublicKey?: Uint8Array\n counter?: number\n }\n\n const credentialId = registrationInfo.credential?.id ?? registrationInfo.credentialID\n const publicKeyBytes = registrationInfo.credential?.publicKey ?? registrationInfo.credentialPublicKey\n const counter = registrationInfo.credential?.counter ?? registrationInfo.counter ?? 0\n const transports = this.normalizeTransports(registrationInfo.credential?.transports)\n\n if (!credentialId || !publicKeyBytes) {\n throw new Error('Passkey registration did not return credential data')\n }\n\n return {\n metadata: {\n credentialId,\n credentialPublicKey: this.bytesToBase64Url(publicKeyBytes),\n counter,\n transports,\n label: parsed.label ?? pending.label ?? 'Passkey',\n },\n }\n }\n\n if (parsed.challenge !== pending.challenge) {\n throw new Error('Invalid passkey setup challenge')\n }\n\n return {\n metadata: {\n credentialId: parsed.credentialId,\n credentialPublicKey: parsed.publicKey,\n counter: 0,\n transports: parsed.transports ?? [],\n label: parsed.label ?? pending.label ?? 'Passkey',\n },\n }\n }\n\n async prepareChallenge(\n userId: string,\n method: MfaMethodRecord,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown>; verifyContext?: MfaVerifyContext }> {\n if (method.userId !== userId) {\n throw new Error('MFA method does not belong to user')\n }\n\n const metadata = method.providerMetadata ?? {}\n const credentialId = typeof metadata.credentialId === 'string' ? metadata.credentialId : null\n if (!credentialId) {\n throw new Error('Passkey credential is not configured')\n }\n\n const securityConfig = this.resolveSecurityConfig(context)\n const options = await generateAuthenticationOptions({\n rpID: this.getRpId(securityConfig),\n userVerification: 'preferred',\n timeout: securityConfig.webauthn.challengeTtlMs,\n allowCredentials: [{\n id: credentialId,\n transports: this.normalizeTransports(metadata.transports),\n }],\n })\n\n const now = Date.now()\n\n return {\n clientData: options as unknown as Record<string, unknown>,\n verifyContext: {\n challenge: {\n challenge: options.challenge,\n createdAt: now,\n },\n },\n }\n }\n\n async verify(\n userId: string,\n method: MfaMethodRecord,\n payload: unknown,\n context?: MfaVerifyContext,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean> {\n const parsed = verifyPayloadSchema.safeParse(payload)\n if (!parsed.success) return false\n if (method.userId !== userId) return false\n\n const parsedContext = verifyContextSchema.safeParse(context)\n if (!parsedContext.success) return false\n\n const securityConfig = this.resolveSecurityConfig(runtimeContext)\n const pending = parsedContext.data.challenge\n if (Date.now() - pending.createdAt > securityConfig.webauthn.challengeTtlMs) {\n return false\n }\n\n const metadata = method.providerMetadata ?? {}\n const credentialId = typeof metadata.credentialId === 'string' ? metadata.credentialId : null\n const credentialPublicKey = typeof metadata.credentialPublicKey === 'string'\n ? metadata.credentialPublicKey\n : typeof metadata.publicKey === 'string'\n ? metadata.publicKey\n : null\n const counter = typeof metadata.counter === 'number' && Number.isFinite(metadata.counter)\n ? metadata.counter\n : 0\n\n if (!credentialId || !credentialPublicKey) {\n return false\n }\n\n const verification = await verifyAuthenticationResponse({\n response: parsed.data.response as never,\n expectedChallenge: pending.challenge,\n expectedOrigin: this.getExpectedOrigins(securityConfig),\n expectedRPID: this.getRpId(securityConfig),\n requireUserVerification: false,\n credential: {\n id: credentialId,\n publicKey: this.base64UrlToBytes(credentialPublicKey),\n counter,\n transports: this.normalizeTransports(metadata.transports),\n },\n }).catch((error: unknown) => {\n logger.warn('Passkey assertion could not be verified', {\n methodId: method.id,\n rpId: this.getRpId(securityConfig),\n err: error,\n })\n return null\n })\n\n if (!verification?.verified) {\n return false\n }\n\n const nextCounter = verification.authenticationInfo?.newCounter\n if (typeof nextCounter === 'number' && Number.isFinite(nextCounter)) {\n const providerMetadata = method.providerMetadata ?? {}\n providerMetadata.counter = nextCounter\n method.providerMetadata = providerMetadata\n }\n\n return true\n }\n\n private normalizeTransports(raw: unknown): AuthenticatorTransportFuture[] {\n if (!Array.isArray(raw)) return []\n return raw.filter((value: unknown): value is AuthenticatorTransportFuture =>\n typeof value === 'string' && AUTHENTICATOR_TRANSPORTS.includes(value as AuthenticatorTransportFuture),\n )\n }\n\n private bytesToBase64Url(value: Uint8Array): string {\n return Buffer.from(value).toString('base64url')\n }\n\n private base64UrlToBytes(value: string) {\n return new Uint8Array(Buffer.from(value, 'base64url')).slice()\n }\n\n private createSetupToken(setup: PendingSetup): string {\n const encodedPayload = Buffer.from(JSON.stringify(setup), 'utf8').toString('base64url')\n const signature = this.signSetupPayload(encodedPayload)\n return `${encodedPayload}.${signature}`\n }\n\n private readSetupToken(token: string): PendingSetup | null {\n const [encodedPayload, signature, ...extra] = token.split('.')\n if (!encodedPayload || !signature || extra.length > 0) {\n return null\n }\n\n const expectedSignature = this.signSetupPayload(encodedPayload)\n const providedBuffer = Buffer.from(signature, 'base64url')\n const expectedBuffer = Buffer.from(expectedSignature, 'base64url')\n if (providedBuffer.length !== expectedBuffer.length || !timingSafeEqual(providedBuffer, expectedBuffer)) {\n return null\n }\n\n try {\n const raw = Buffer.from(encodedPayload, 'base64url').toString('utf8')\n const parsed = JSON.parse(raw)\n const result = setupTokenSchema.safeParse(parsed)\n return result.success ? result.data : null\n } catch {\n return null\n }\n }\n\n private signSetupPayload(encodedPayload: string): string {\n return createHmac('sha256', this.getSetupTokenSecret()).update(encodedPayload).digest('base64url')\n }\n\n private getSetupTokenSecret(): string {\n return this.setupTokenSecret\n }\n\n private getRpName(securityConfig: SecurityModuleConfig): string {\n return securityConfig.webauthn.rpName\n }\n\n private getRpId(securityConfig: SecurityModuleConfig): string {\n return securityConfig.webauthn.rpId\n }\n\n private getExpectedOrigins(securityConfig: SecurityModuleConfig): string[] {\n return [...securityConfig.webauthn.expectedOrigins]\n }\n\n private resolveSecurityConfig(context?: MfaProviderRuntimeContext): SecurityModuleConfig {\n return resolveSecurityModuleConfigForRequest(this.securityConfig, context?.request)\n }\n\n private toWebAuthnUserId(userId: string) {\n return new TextEncoder().encode(userId).slice()\n }\n}\n\nexport default PasskeyProvider\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6977.1.5041da373e",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@open-mercato/core": "0.6.8-develop.
|
|
69
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
68
|
+
"@open-mercato/core": "0.6.8-develop.6977.1.5041da373e",
|
|
69
|
+
"@open-mercato/ui": "0.6.8-develop.6977.1.5041da373e",
|
|
70
70
|
"@simplewebauthn/browser": "^13.3.0",
|
|
71
71
|
"@simplewebauthn/server": "^13.3.2",
|
|
72
72
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
"qrcode": "^1.5.4"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
79
|
+
"@open-mercato/shared": "0.6.8-develop.6977.1.5041da373e",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"react-dom": "^19.0.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
84
|
+
"@open-mercato/shared": "0.6.8-develop.6977.1.5041da373e",
|
|
85
85
|
"@types/jest": "^30.0.0",
|
|
86
86
|
"@types/react": "^19.2.17",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
@@ -4,7 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import { GitPullRequestArrow, Calendar } from 'lucide-react'
|
|
5
5
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
6
|
import { formatRelativeTime } from '@open-mercato/shared/lib/time'
|
|
7
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
8
|
import type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'
|
|
9
9
|
|
|
10
10
|
type ChangeRow = {
|
|
@@ -35,6 +35,7 @@ function parseRows(notification: NotificationRendererProps['notification']): Cha
|
|
|
35
35
|
|
|
36
36
|
export function IncomingChangesRenderer({ notification }: NotificationRendererProps) {
|
|
37
37
|
const t = useT()
|
|
38
|
+
const locale = useLocale()
|
|
38
39
|
const rows = React.useMemo(() => parseRows(notification), [notification])
|
|
39
40
|
const isUnread = notification.status === 'unread'
|
|
40
41
|
|
|
@@ -63,7 +64,7 @@ export function IncomingChangesRenderer({ notification }: NotificationRendererPr
|
|
|
63
64
|
</h4>
|
|
64
65
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
65
66
|
<Calendar className="h-3 w-3" />
|
|
66
|
-
{formatRelativeTime(notification.createdAt, { translate: t }) ?? ''}
|
|
67
|
+
{formatRelativeTime(notification.createdAt, { locale, translate: t }) ?? ''}
|
|
67
68
|
</span>
|
|
68
69
|
</div>
|
|
69
70
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
verifyRegistrationResponse,
|
|
7
7
|
} from '@simplewebauthn/server'
|
|
8
8
|
import type { AuthenticatorTransportFuture } from '@simplewebauthn/types'
|
|
9
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
9
10
|
import { z } from 'zod'
|
|
10
11
|
import type {
|
|
11
12
|
MfaMethodRecord,
|
|
@@ -24,6 +25,8 @@ import {
|
|
|
24
25
|
|
|
25
26
|
const SETUP_TOKEN_VERSION = 'v1'
|
|
26
27
|
|
|
28
|
+
const logger = createLogger('security').child({ component: 'passkey-provider' })
|
|
29
|
+
|
|
27
30
|
const setupPayloadSchema = z.object({
|
|
28
31
|
label: z.string().min(1).max(100).optional(),
|
|
29
32
|
userName: z.string().min(1).max(255).optional(),
|
|
@@ -44,15 +47,9 @@ const setupConfirmationPayloadSchema = z.union([
|
|
|
44
47
|
}),
|
|
45
48
|
])
|
|
46
49
|
|
|
47
|
-
const verifyPayloadSchema = z.
|
|
48
|
-
z.
|
|
49
|
-
|
|
50
|
-
}),
|
|
51
|
-
z.object({
|
|
52
|
-
credentialId: z.string().min(1),
|
|
53
|
-
challenge: z.string().min(1),
|
|
54
|
-
}),
|
|
55
|
-
])
|
|
50
|
+
const verifyPayloadSchema = z.object({
|
|
51
|
+
response: z.record(z.string(), z.unknown()),
|
|
52
|
+
})
|
|
56
53
|
|
|
57
54
|
const setupTokenSchema = z.object({
|
|
58
55
|
version: z.string().min(1),
|
|
@@ -275,73 +272,64 @@ export class PasskeyProvider implements MfaProviderInterface {
|
|
|
275
272
|
context?: MfaVerifyContext,
|
|
276
273
|
runtimeContext?: MfaProviderRuntimeContext,
|
|
277
274
|
): Promise<boolean> {
|
|
278
|
-
const parsed = verifyPayloadSchema.
|
|
275
|
+
const parsed = verifyPayloadSchema.safeParse(payload)
|
|
276
|
+
if (!parsed.success) return false
|
|
279
277
|
if (method.userId !== userId) return false
|
|
280
278
|
|
|
281
279
|
const parsedContext = verifyContextSchema.safeParse(context)
|
|
280
|
+
if (!parsedContext.success) return false
|
|
281
|
+
|
|
282
282
|
const securityConfig = this.resolveSecurityConfig(runtimeContext)
|
|
283
|
-
if (!parsedContext.success) {
|
|
284
|
-
if ('credentialId' in parsed) {
|
|
285
|
-
const metadataCredentialId = method.providerMetadata?.credentialId
|
|
286
|
-
return typeof metadataCredentialId === 'string' && metadataCredentialId === parsed.credentialId
|
|
287
|
-
}
|
|
288
|
-
return false
|
|
289
|
-
}
|
|
290
283
|
const pending = parsedContext.data.challenge
|
|
291
284
|
if (Date.now() - pending.createdAt > securityConfig.webauthn.challengeTtlMs) {
|
|
292
285
|
return false
|
|
293
286
|
}
|
|
294
287
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
288
|
+
const metadata = method.providerMetadata ?? {}
|
|
289
|
+
const credentialId = typeof metadata.credentialId === 'string' ? metadata.credentialId : null
|
|
290
|
+
const credentialPublicKey = typeof metadata.credentialPublicKey === 'string'
|
|
291
|
+
? metadata.credentialPublicKey
|
|
292
|
+
: typeof metadata.publicKey === 'string'
|
|
293
|
+
? metadata.publicKey
|
|
294
|
+
: null
|
|
295
|
+
const counter = typeof metadata.counter === 'number' && Number.isFinite(metadata.counter)
|
|
296
|
+
? metadata.counter
|
|
297
|
+
: 0
|
|
298
|
+
|
|
299
|
+
if (!credentialId || !credentialPublicKey) {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
310
302
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
303
|
+
const verification = await verifyAuthenticationResponse({
|
|
304
|
+
response: parsed.data.response as never,
|
|
305
|
+
expectedChallenge: pending.challenge,
|
|
306
|
+
expectedOrigin: this.getExpectedOrigins(securityConfig),
|
|
307
|
+
expectedRPID: this.getRpId(securityConfig),
|
|
308
|
+
requireUserVerification: false,
|
|
309
|
+
credential: {
|
|
310
|
+
id: credentialId,
|
|
311
|
+
publicKey: this.base64UrlToBytes(credentialPublicKey),
|
|
312
|
+
counter,
|
|
313
|
+
transports: this.normalizeTransports(metadata.transports),
|
|
314
|
+
},
|
|
315
|
+
}).catch((error: unknown) => {
|
|
316
|
+
logger.warn('Passkey assertion could not be verified', {
|
|
317
|
+
methodId: method.id,
|
|
318
|
+
rpId: this.getRpId(securityConfig),
|
|
319
|
+
err: error,
|
|
323
320
|
})
|
|
321
|
+
return null
|
|
322
|
+
})
|
|
324
323
|
|
|
325
|
-
|
|
326
|
-
return false
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const nextCounter = verification.authenticationInfo?.newCounter
|
|
330
|
-
if (typeof nextCounter === 'number' && Number.isFinite(nextCounter)) {
|
|
331
|
-
const providerMetadata = method.providerMetadata ?? {}
|
|
332
|
-
providerMetadata.counter = nextCounter
|
|
333
|
-
method.providerMetadata = providerMetadata
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return true
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const metadataCredentialId = method.providerMetadata?.credentialId
|
|
340
|
-
if (typeof metadataCredentialId !== 'string' || metadataCredentialId !== parsed.credentialId) {
|
|
324
|
+
if (!verification?.verified) {
|
|
341
325
|
return false
|
|
342
326
|
}
|
|
343
|
-
|
|
344
|
-
|
|
327
|
+
|
|
328
|
+
const nextCounter = verification.authenticationInfo?.newCounter
|
|
329
|
+
if (typeof nextCounter === 'number' && Number.isFinite(nextCounter)) {
|
|
330
|
+
const providerMetadata = method.providerMetadata ?? {}
|
|
331
|
+
providerMetadata.counter = nextCounter
|
|
332
|
+
method.providerMetadata = providerMetadata
|
|
345
333
|
}
|
|
346
334
|
|
|
347
335
|
return true
|