@arcblock/did-connect-service 4.0.0-beta.8
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/_generated/did-address-bundle-string.d.ts +3 -0
- package/dist/_generated/did-address-bundle-string.d.ts.map +1 -0
- package/dist/_generated/did-address-bundle-string.js +3 -0
- package/dist/_generated/did-address-bundle-string.js.map +1 -0
- package/dist/_generated/header-bundle-string.d.ts +3 -0
- package/dist/_generated/header-bundle-string.d.ts.map +1 -0
- package/dist/_generated/header-bundle-string.js +3 -0
- package/dist/_generated/header-bundle-string.js.map +1 -0
- package/dist/_generated/login-bundle-string.d.ts +3 -0
- package/dist/_generated/login-bundle-string.d.ts.map +1 -0
- package/dist/_generated/login-bundle-string.js +3 -0
- package/dist/_generated/login-bundle-string.js.map +1 -0
- package/dist/_generated/qr-bundle-string.d.ts +3 -0
- package/dist/_generated/qr-bundle-string.d.ts.map +1 -0
- package/dist/_generated/qr-bundle-string.js +3 -0
- package/dist/_generated/qr-bundle-string.js.map +1 -0
- package/dist/access/access-key-util.d.ts +19 -0
- package/dist/access/access-key-util.d.ts.map +1 -0
- package/dist/access/access-key-util.js +45 -0
- package/dist/access/access-key-util.js.map +1 -0
- package/dist/access/access-policy.d.ts +55 -0
- package/dist/access/access-policy.d.ts.map +1 -0
- package/dist/access/access-policy.js +177 -0
- package/dist/access/access-policy.js.map +1 -0
- package/dist/access/login-access-check.d.ts +27 -0
- package/dist/access/login-access-check.d.ts.map +1 -0
- package/dist/access/login-access-check.js +34 -0
- package/dist/access/login-access-check.js.map +1 -0
- package/dist/access/rbac.d.ts +19 -0
- package/dist/access/rbac.d.ts.map +1 -0
- package/dist/access/rbac.js +79 -0
- package/dist/access/rbac.js.map +1 -0
- package/dist/access-key-handler.d.ts +37 -0
- package/dist/access-key-handler.d.ts.map +1 -0
- package/dist/access-key-handler.js +316 -0
- package/dist/access-key-handler.js.map +1 -0
- package/dist/access-key-util.d.ts +19 -0
- package/dist/access-key-util.d.ts.map +1 -0
- package/dist/access-key-util.js +45 -0
- package/dist/access-key-util.js.map +1 -0
- package/dist/access-policy.d.ts +53 -0
- package/dist/access-policy.d.ts.map +1 -0
- package/dist/access-policy.js +153 -0
- package/dist/access-policy.js.map +1 -0
- package/dist/auth-client.d.ts +20 -0
- package/dist/auth-client.d.ts.map +1 -0
- package/dist/auth-client.js +42 -0
- package/dist/auth-client.js.map +1 -0
- package/dist/auth-entrypoint.d.ts +45 -0
- package/dist/auth-entrypoint.d.ts.map +1 -0
- package/dist/auth-entrypoint.js +31 -0
- package/dist/auth-entrypoint.js.map +1 -0
- package/dist/auth-handler.d.ts +136 -0
- package/dist/auth-handler.d.ts.map +1 -0
- package/dist/auth-handler.js +408 -0
- package/dist/auth-handler.js.map +1 -0
- package/dist/auth-rpc-types.d.ts +139 -0
- package/dist/auth-rpc-types.d.ts.map +1 -0
- package/dist/auth-rpc-types.js +11 -0
- package/dist/auth-rpc-types.js.map +1 -0
- package/dist/auth-rpc.d.ts +80 -0
- package/dist/auth-rpc.d.ts.map +1 -0
- package/dist/auth-rpc.js +257 -0
- package/dist/auth-rpc.js.map +1 -0
- package/dist/auth-worker.d.ts +42 -0
- package/dist/auth-worker.d.ts.map +1 -0
- package/dist/auth-worker.js +120 -0
- package/dist/auth-worker.js.map +1 -0
- package/dist/blocklet-js-handler.d.ts +22 -0
- package/dist/blocklet-js-handler.d.ts.map +1 -0
- package/dist/blocklet-js-handler.js +205 -0
- package/dist/blocklet-js-handler.js.map +1 -0
- package/dist/blocklet-service-client.d.ts +80 -0
- package/dist/blocklet-service-client.d.ts.map +1 -0
- package/dist/blocklet-service-client.js +130 -0
- package/dist/blocklet-service-client.js.map +1 -0
- package/dist/blocklet-service-rpc-types.d.ts +153 -0
- package/dist/blocklet-service-rpc-types.d.ts.map +1 -0
- package/dist/blocklet-service-rpc-types.js +11 -0
- package/dist/blocklet-service-rpc-types.js.map +1 -0
- package/dist/blocklet-service-rpc.d.ts +92 -0
- package/dist/blocklet-service-rpc.d.ts.map +1 -0
- package/dist/blocklet-service-rpc.js +410 -0
- package/dist/blocklet-service-rpc.js.map +1 -0
- package/dist/blocklet-service.d.ts +57 -0
- package/dist/blocklet-service.d.ts.map +1 -0
- package/dist/blocklet-service.js +377 -0
- package/dist/blocklet-service.js.map +1 -0
- package/dist/branding-handler.d.ts +42 -0
- package/dist/branding-handler.d.ts.map +1 -0
- package/dist/branding-handler.js +326 -0
- package/dist/branding-handler.js.map +1 -0
- package/dist/constants.d.ts +18 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +17 -0
- package/dist/constants.js.map +1 -0
- package/dist/crypto/aes-gcm.d.ts +18 -0
- package/dist/crypto/aes-gcm.d.ts.map +1 -0
- package/dist/crypto/aes-gcm.js +46 -0
- package/dist/crypto/aes-gcm.js.map +1 -0
- package/dist/d1-token-storage.d.ts +31 -0
- package/dist/d1-token-storage.d.ts.map +1 -0
- package/dist/d1-token-storage.js +83 -0
- package/dist/d1-token-storage.js.map +1 -0
- package/dist/did-connect-handler.d.ts +57 -0
- package/dist/did-connect-handler.d.ts.map +1 -0
- package/dist/did-connect-handler.js +182 -0
- package/dist/did-connect-handler.js.map +1 -0
- package/dist/did.d.ts +14 -0
- package/dist/did.d.ts.map +1 -0
- package/dist/did.js +17 -0
- package/dist/did.js.map +1 -0
- package/dist/email-login-handler.d.ts +50 -0
- package/dist/email-login-handler.d.ts.map +1 -0
- package/dist/email-login-handler.js +238 -0
- package/dist/email-login-handler.js.map +1 -0
- package/dist/embedded.d.ts +25 -0
- package/dist/embedded.d.ts.map +1 -0
- package/dist/embedded.js +21 -0
- package/dist/embedded.js.map +1 -0
- package/dist/federation-utils.d.ts +23 -0
- package/dist/federation-utils.d.ts.map +1 -0
- package/dist/federation-utils.js +25 -0
- package/dist/federation-utils.js.map +1 -0
- package/dist/handler.d.ts +90 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +591 -0
- package/dist/handler.js.map +1 -0
- package/dist/handlers/access-key-connect-handler.d.ts +40 -0
- package/dist/handlers/access-key-connect-handler.d.ts.map +1 -0
- package/dist/handlers/access-key-connect-handler.js +153 -0
- package/dist/handlers/access-key-connect-handler.js.map +1 -0
- package/dist/handlers/access-key-handler.d.ts +54 -0
- package/dist/handlers/access-key-handler.d.ts.map +1 -0
- package/dist/handlers/access-key-handler.js +336 -0
- package/dist/handlers/access-key-handler.js.map +1 -0
- package/dist/handlers/admin-instance-handler.d.ts +29 -0
- package/dist/handlers/admin-instance-handler.d.ts.map +1 -0
- package/dist/handlers/admin-instance-handler.js +156 -0
- package/dist/handlers/admin-instance-handler.js.map +1 -0
- package/dist/handlers/auth-handler.d.ts +151 -0
- package/dist/handlers/auth-handler.d.ts.map +1 -0
- package/dist/handlers/auth-handler.js +873 -0
- package/dist/handlers/auth-handler.js.map +1 -0
- package/dist/handlers/avatar-handler.d.ts +42 -0
- package/dist/handlers/avatar-handler.d.ts.map +1 -0
- package/dist/handlers/avatar-handler.js +179 -0
- package/dist/handlers/avatar-handler.js.map +1 -0
- package/dist/handlers/blocklet-js-handler.d.ts +26 -0
- package/dist/handlers/blocklet-js-handler.d.ts.map +1 -0
- package/dist/handlers/blocklet-js-handler.js +226 -0
- package/dist/handlers/blocklet-js-handler.js.map +1 -0
- package/dist/handlers/branding-handler.d.ts +45 -0
- package/dist/handlers/branding-handler.d.ts.map +1 -0
- package/dist/handlers/branding-handler.js +392 -0
- package/dist/handlers/branding-handler.js.map +1 -0
- package/dist/handlers/did-connect-handler.d.ts +81 -0
- package/dist/handlers/did-connect-handler.d.ts.map +1 -0
- package/dist/handlers/did-connect-handler.js +384 -0
- package/dist/handlers/did-connect-handler.js.map +1 -0
- package/dist/handlers/email-login-handler.d.ts +53 -0
- package/dist/handlers/email-login-handler.d.ts.map +1 -0
- package/dist/handlers/email-login-handler.js +320 -0
- package/dist/handlers/email-login-handler.js.map +1 -0
- package/dist/handlers/federation-admin-handler.d.ts +69 -0
- package/dist/handlers/federation-admin-handler.d.ts.map +1 -0
- package/dist/handlers/federation-admin-handler.js +602 -0
- package/dist/handlers/federation-admin-handler.js.map +1 -0
- package/dist/handlers/membership-handler.d.ts +27 -0
- package/dist/handlers/membership-handler.d.ts.map +1 -0
- package/dist/handlers/membership-handler.js +122 -0
- package/dist/handlers/membership-handler.js.map +1 -0
- package/dist/handlers/oauth-handler.d.ts +79 -0
- package/dist/handlers/oauth-handler.d.ts.map +1 -0
- package/dist/handlers/oauth-handler.js +870 -0
- package/dist/handlers/oauth-handler.js.map +1 -0
- package/dist/handlers/passkey-handler.d.ts +112 -0
- package/dist/handlers/passkey-handler.d.ts.map +1 -0
- package/dist/handlers/passkey-handler.js +1020 -0
- package/dist/handlers/passkey-handler.js.map +1 -0
- package/dist/handlers/team-handler.d.ts +120 -0
- package/dist/handlers/team-handler.d.ts.map +1 -0
- package/dist/handlers/team-handler.js +1750 -0
- package/dist/handlers/team-handler.js.map +1 -0
- package/dist/handlers/ticket-handler.d.ts +33 -0
- package/dist/handlers/ticket-handler.d.ts.map +1 -0
- package/dist/handlers/ticket-handler.js +131 -0
- package/dist/handlers/ticket-handler.js.map +1 -0
- package/dist/identity/auth-entrypoint.d.ts +45 -0
- package/dist/identity/auth-entrypoint.d.ts.map +1 -0
- package/dist/identity/auth-entrypoint.js +32 -0
- package/dist/identity/auth-entrypoint.js.map +1 -0
- package/dist/identity/auto-membership.d.ts +16 -0
- package/dist/identity/auto-membership.d.ts.map +1 -0
- package/dist/identity/auto-membership.js +52 -0
- package/dist/identity/auto-membership.js.map +1 -0
- package/dist/identity/federation.d.ts +23 -0
- package/dist/identity/federation.d.ts.map +1 -0
- package/dist/identity/federation.js +26 -0
- package/dist/identity/federation.js.map +1 -0
- package/dist/identity/gravatar.d.ts +14 -0
- package/dist/identity/gravatar.d.ts.map +1 -0
- package/dist/identity/gravatar.js +132 -0
- package/dist/identity/gravatar.js.map +1 -0
- package/dist/identity/instance-role.d.ts +10 -0
- package/dist/identity/instance-role.d.ts.map +1 -0
- package/dist/identity/instance-role.js +20 -0
- package/dist/identity/instance-role.js.map +1 -0
- package/dist/identity/invitation-util.d.ts +7 -0
- package/dist/identity/invitation-util.d.ts.map +1 -0
- package/dist/identity/invitation-util.js +66 -0
- package/dist/identity/invitation-util.js.map +1 -0
- package/dist/identity/jwt.d.ts +7 -0
- package/dist/identity/jwt.d.ts.map +1 -0
- package/dist/identity/jwt.js +72 -0
- package/dist/identity/jwt.js.map +1 -0
- package/dist/identity/passkey-did.d.ts +14 -0
- package/dist/identity/passkey-did.d.ts.map +1 -0
- package/dist/identity/passkey-did.js +17 -0
- package/dist/identity/passkey-did.js.map +1 -0
- package/dist/identity/session-context.d.ts +35 -0
- package/dist/identity/session-context.d.ts.map +1 -0
- package/dist/identity/session-context.js +39 -0
- package/dist/identity/session-context.js.map +1 -0
- package/dist/identity/sign-response.d.ts +31 -0
- package/dist/identity/sign-response.d.ts.map +1 -0
- package/dist/identity/sign-response.js +62 -0
- package/dist/identity/sign-response.js.map +1 -0
- package/dist/identity/wallet-identity.d.ts +71 -0
- package/dist/identity/wallet-identity.d.ts.map +1 -0
- package/dist/identity/wallet-identity.js +97 -0
- package/dist/identity/wallet-identity.js.map +1 -0
- package/dist/identity/webauthn.d.ts +69 -0
- package/dist/identity/webauthn.d.ts.map +1 -0
- package/dist/identity/webauthn.js +113 -0
- package/dist/identity/webauthn.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/instance-role.d.ts +10 -0
- package/dist/instance-role.d.ts.map +1 -0
- package/dist/instance-role.js +20 -0
- package/dist/instance-role.js.map +1 -0
- package/dist/jwt.d.ts +7 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +72 -0
- package/dist/jwt.js.map +1 -0
- package/dist/locale.d.ts +17 -0
- package/dist/locale.d.ts.map +1 -0
- package/dist/locale.js +48 -0
- package/dist/locale.js.map +1 -0
- package/dist/login-entry.d.ts +9 -0
- package/dist/login-entry.d.ts.map +1 -0
- package/dist/login-entry.js +9 -0
- package/dist/login-entry.js.map +1 -0
- package/dist/membership-handler.d.ts +27 -0
- package/dist/membership-handler.d.ts.map +1 -0
- package/dist/membership-handler.js +111 -0
- package/dist/membership-handler.js.map +1 -0
- package/dist/oauth-adapters/apple.d.ts +36 -0
- package/dist/oauth-adapters/apple.d.ts.map +1 -0
- package/dist/oauth-adapters/apple.js +127 -0
- package/dist/oauth-adapters/apple.js.map +1 -0
- package/dist/oauth-adapters/auth0-legacy.d.ts +28 -0
- package/dist/oauth-adapters/auth0-legacy.d.ts.map +1 -0
- package/dist/oauth-adapters/auth0-legacy.js +63 -0
- package/dist/oauth-adapters/auth0-legacy.js.map +1 -0
- package/dist/oauth-adapters/auth0.d.ts +24 -0
- package/dist/oauth-adapters/auth0.d.ts.map +1 -0
- package/dist/oauth-adapters/auth0.js +59 -0
- package/dist/oauth-adapters/auth0.js.map +1 -0
- package/dist/oauth-adapters/facebook.d.ts +20 -0
- package/dist/oauth-adapters/facebook.d.ts.map +1 -0
- package/dist/oauth-adapters/facebook.js +52 -0
- package/dist/oauth-adapters/facebook.js.map +1 -0
- package/dist/oauth-adapters/github.d.ts +20 -0
- package/dist/oauth-adapters/github.d.ts.map +1 -0
- package/dist/oauth-adapters/github.js +78 -0
- package/dist/oauth-adapters/github.js.map +1 -0
- package/dist/oauth-adapters/google.d.ts +21 -0
- package/dist/oauth-adapters/google.d.ts.map +1 -0
- package/dist/oauth-adapters/google.js +61 -0
- package/dist/oauth-adapters/google.js.map +1 -0
- package/dist/oauth-adapters/jwks-cache.d.ts +24 -0
- package/dist/oauth-adapters/jwks-cache.d.ts.map +1 -0
- package/dist/oauth-adapters/jwks-cache.js +77 -0
- package/dist/oauth-adapters/jwks-cache.js.map +1 -0
- package/dist/oauth-adapters/twitter.d.ts +23 -0
- package/dist/oauth-adapters/twitter.d.ts.map +1 -0
- package/dist/oauth-adapters/twitter.js +65 -0
- package/dist/oauth-adapters/twitter.js.map +1 -0
- package/dist/oauth-adapters/types.d.ts +60 -0
- package/dist/oauth-adapters/types.d.ts.map +1 -0
- package/dist/oauth-adapters/types.js +8 -0
- package/dist/oauth-adapters/types.js.map +1 -0
- package/dist/oauth-callback-page.d.ts +9 -0
- package/dist/oauth-callback-page.d.ts.map +1 -0
- package/dist/oauth-callback-page.js +31 -0
- package/dist/oauth-callback-page.js.map +1 -0
- package/dist/oauth-handler.d.ts +72 -0
- package/dist/oauth-handler.d.ts.map +1 -0
- package/dist/oauth-handler.js +423 -0
- package/dist/oauth-handler.js.map +1 -0
- package/dist/page.d.ts +33 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +59 -0
- package/dist/page.js.map +1 -0
- package/dist/pages/admin/i18n.d.ts +31 -0
- package/dist/pages/admin/i18n.d.ts.map +1 -0
- package/dist/pages/admin/i18n.js +1345 -0
- package/dist/pages/admin/i18n.js.map +1 -0
- package/dist/pages/admin/index.d.ts +36 -0
- package/dist/pages/admin/index.d.ts.map +1 -0
- package/dist/pages/admin/index.js +418 -0
- package/dist/pages/admin/index.js.map +1 -0
- package/dist/pages/admin/scripts/api-client.d.ts +6 -0
- package/dist/pages/admin/scripts/api-client.d.ts.map +1 -0
- package/dist/pages/admin/scripts/api-client.js +38 -0
- package/dist/pages/admin/scripts/api-client.js.map +1 -0
- package/dist/pages/admin/scripts/cropper.d.ts +8 -0
- package/dist/pages/admin/scripts/cropper.d.ts.map +1 -0
- package/dist/pages/admin/scripts/cropper.js +222 -0
- package/dist/pages/admin/scripts/cropper.js.map +1 -0
- package/dist/pages/admin/scripts/dialog.d.ts +5 -0
- package/dist/pages/admin/scripts/dialog.d.ts.map +1 -0
- package/dist/pages/admin/scripts/dialog.js +88 -0
- package/dist/pages/admin/scripts/dialog.js.map +1 -0
- package/dist/pages/admin/scripts/router.d.ts +5 -0
- package/dist/pages/admin/scripts/router.d.ts.map +1 -0
- package/dist/pages/admin/scripts/router.js +54 -0
- package/dist/pages/admin/scripts/router.js.map +1 -0
- package/dist/pages/admin/scripts/toast.d.ts +5 -0
- package/dist/pages/admin/scripts/toast.d.ts.map +1 -0
- package/dist/pages/admin/scripts/toast.js +30 -0
- package/dist/pages/admin/scripts/toast.js.map +1 -0
- package/dist/pages/admin/scripts/utils.d.ts +5 -0
- package/dist/pages/admin/scripts/utils.d.ts.map +1 -0
- package/dist/pages/admin/scripts/utils.js +108 -0
- package/dist/pages/admin/scripts/utils.js.map +1 -0
- package/dist/pages/admin/styles.d.ts +9 -0
- package/dist/pages/admin/styles.d.ts.map +1 -0
- package/dist/pages/admin/styles.js +2223 -0
- package/dist/pages/admin/styles.js.map +1 -0
- package/dist/pages/admin/tab-access-keys.d.ts +8 -0
- package/dist/pages/admin/tab-access-keys.d.ts.map +1 -0
- package/dist/pages/admin/tab-access-keys.js +255 -0
- package/dist/pages/admin/tab-access-keys.js.map +1 -0
- package/dist/pages/admin/tab-access.d.ts +14 -0
- package/dist/pages/admin/tab-access.d.ts.map +1 -0
- package/dist/pages/admin/tab-access.js +420 -0
- package/dist/pages/admin/tab-access.js.map +1 -0
- package/dist/pages/admin/tab-appearance.d.ts +9 -0
- package/dist/pages/admin/tab-appearance.d.ts.map +1 -0
- package/dist/pages/admin/tab-appearance.js +298 -0
- package/dist/pages/admin/tab-appearance.js.map +1 -0
- package/dist/pages/admin/tab-audit.d.ts +8 -0
- package/dist/pages/admin/tab-audit.d.ts.map +1 -0
- package/dist/pages/admin/tab-audit.js +289 -0
- package/dist/pages/admin/tab-audit.js.map +1 -0
- package/dist/pages/admin/tab-branding.d.ts +9 -0
- package/dist/pages/admin/tab-branding.d.ts.map +1 -0
- package/dist/pages/admin/tab-branding.js +486 -0
- package/dist/pages/admin/tab-branding.js.map +1 -0
- package/dist/pages/admin/tab-federation.d.ts +8 -0
- package/dist/pages/admin/tab-federation.d.ts.map +1 -0
- package/dist/pages/admin/tab-federation.js +416 -0
- package/dist/pages/admin/tab-federation.js.map +1 -0
- package/dist/pages/admin/tab-invitations.d.ts +8 -0
- package/dist/pages/admin/tab-invitations.d.ts.map +1 -0
- package/dist/pages/admin/tab-invitations.js +161 -0
- package/dist/pages/admin/tab-invitations.js.map +1 -0
- package/dist/pages/admin/tab-members.d.ts +8 -0
- package/dist/pages/admin/tab-members.d.ts.map +1 -0
- package/dist/pages/admin/tab-members.js +575 -0
- package/dist/pages/admin/tab-members.js.map +1 -0
- package/dist/pages/admin/tab-profile-accounts.d.ts +9 -0
- package/dist/pages/admin/tab-profile-accounts.d.ts.map +1 -0
- package/dist/pages/admin/tab-profile-accounts.js +580 -0
- package/dist/pages/admin/tab-profile-accounts.js.map +1 -0
- package/dist/pages/admin/tab-profile.d.ts +8 -0
- package/dist/pages/admin/tab-profile.d.ts.map +1 -0
- package/dist/pages/admin/tab-profile.js +383 -0
- package/dist/pages/admin/tab-profile.js.map +1 -0
- package/dist/pages/admin/tab-settings.d.ts +9 -0
- package/dist/pages/admin/tab-settings.d.ts.map +1 -0
- package/dist/pages/admin/tab-settings.js +486 -0
- package/dist/pages/admin/tab-settings.js.map +1 -0
- package/dist/pages/admin-instances-page.d.ts +8 -0
- package/dist/pages/admin-instances-page.d.ts.map +1 -0
- package/dist/pages/admin-instances-page.js +386 -0
- package/dist/pages/admin-instances-page.js.map +1 -0
- package/dist/pages/auth-script.d.ts +18 -0
- package/dist/pages/auth-script.d.ts.map +1 -0
- package/dist/pages/auth-script.js +185 -0
- package/dist/pages/auth-script.js.map +1 -0
- package/dist/pages/design-tokens.d.ts +86 -0
- package/dist/pages/design-tokens.d.ts.map +1 -0
- package/dist/pages/design-tokens.js +159 -0
- package/dist/pages/design-tokens.js.map +1 -0
- package/dist/pages/did-address-bundle-entry.d.ts +14 -0
- package/dist/pages/did-address-bundle-entry.d.ts.map +1 -0
- package/dist/pages/did-address-bundle-entry.js +20 -0
- package/dist/pages/did-address-bundle-entry.js.map +1 -0
- package/dist/pages/did-connect-script.d.ts +16 -0
- package/dist/pages/did-connect-script.d.ts.map +1 -0
- package/dist/pages/did-connect-script.js +105 -0
- package/dist/pages/did-connect-script.js.map +1 -0
- package/dist/pages/error-page.d.ts +21 -0
- package/dist/pages/error-page.d.ts.map +1 -0
- package/dist/pages/error-page.js +103 -0
- package/dist/pages/error-page.js.map +1 -0
- package/dist/pages/gen-access-key-page.d.ts +27 -0
- package/dist/pages/gen-access-key-page.d.ts.map +1 -0
- package/dist/pages/gen-access-key-page.js +406 -0
- package/dist/pages/gen-access-key-page.js.map +1 -0
- package/dist/pages/header-bundle-entry.d.ts +2 -0
- package/dist/pages/header-bundle-entry.d.ts.map +1 -0
- package/dist/pages/header-bundle-entry.js +4 -0
- package/dist/pages/header-bundle-entry.js.map +1 -0
- package/dist/pages/homepage.d.ts +17 -0
- package/dist/pages/homepage.d.ts.map +1 -0
- package/dist/pages/homepage.js +407 -0
- package/dist/pages/homepage.js.map +1 -0
- package/dist/pages/invite-page.d.ts +16 -0
- package/dist/pages/invite-page.d.ts.map +1 -0
- package/dist/pages/invite-page.js +241 -0
- package/dist/pages/invite-page.js.map +1 -0
- package/dist/pages/login-bundle-entry.d.ts +9 -0
- package/dist/pages/login-bundle-entry.d.ts.map +1 -0
- package/dist/pages/login-bundle-entry.js +9 -0
- package/dist/pages/login-bundle-entry.js.map +1 -0
- package/dist/pages/login-page.d.ts +37 -0
- package/dist/pages/login-page.d.ts.map +1 -0
- package/dist/pages/login-page.js +93 -0
- package/dist/pages/login-page.js.map +1 -0
- package/dist/pages/oauth-callback-page.d.ts +16 -0
- package/dist/pages/oauth-callback-page.d.ts.map +1 -0
- package/dist/pages/oauth-callback-page.js +84 -0
- package/dist/pages/oauth-callback-page.js.map +1 -0
- package/dist/pages/qr-bundle-entry.d.ts +6 -0
- package/dist/pages/qr-bundle-entry.d.ts.map +1 -0
- package/dist/pages/qr-bundle-entry.js +7 -0
- package/dist/pages/qr-bundle-entry.js.map +1 -0
- package/dist/pages/shared-styles.d.ts +6 -0
- package/dist/pages/shared-styles.d.ts.map +1 -0
- package/dist/pages/shared-styles.js +109 -0
- package/dist/pages/shared-styles.js.map +1 -0
- package/dist/rbac.d.ts +19 -0
- package/dist/rbac.d.ts.map +1 -0
- package/dist/rbac.js +76 -0
- package/dist/rbac.js.map +1 -0
- package/dist/session-context.d.ts +35 -0
- package/dist/session-context.d.ts.map +1 -0
- package/dist/session-context.js +39 -0
- package/dist/session-context.js.map +1 -0
- package/dist/store/d1-compat.d.ts +21 -0
- package/dist/store/d1-compat.d.ts.map +1 -0
- package/dist/store/d1-compat.js +111 -0
- package/dist/store/d1-compat.js.map +1 -0
- package/dist/store/d1-store.d.ts +348 -0
- package/dist/store/d1-store.d.ts.map +1 -0
- package/dist/store/d1-store.js +1587 -0
- package/dist/store/d1-store.js.map +1 -0
- package/dist/store/d1-token-storage.d.ts +31 -0
- package/dist/store/d1-token-storage.d.ts.map +1 -0
- package/dist/store/d1-token-storage.js +92 -0
- package/dist/store/d1-token-storage.js.map +1 -0
- package/dist/store.d.ts +222 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +1366 -0
- package/dist/store.js.map +1 -0
- package/dist/team-handler.d.ts +90 -0
- package/dist/team-handler.d.ts.map +1 -0
- package/dist/team-handler.js +1225 -0
- package/dist/team-handler.js.map +1 -0
- package/dist/theme-utils.d.ts +195 -0
- package/dist/theme-utils.d.ts.map +1 -0
- package/dist/theme-utils.js +132 -0
- package/dist/theme-utils.js.map +1 -0
- package/dist/ticket-handler.d.ts +28 -0
- package/dist/ticket-handler.d.ts.map +1 -0
- package/dist/ticket-handler.js +74 -0
- package/dist/ticket-handler.js.map +1 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/wallet-identity.d.ts +32 -0
- package/dist/wallet-identity.d.ts.map +1 -0
- package/dist/wallet-identity.js +43 -0
- package/dist/wallet-identity.js.map +1 -0
- package/dist/webauthn.d.ts +65 -0
- package/dist/webauthn.d.ts.map +1 -0
- package/dist/webauthn.js +112 -0
- package/dist/webauthn.js.map +1 -0
- package/migrations/0001_initial_schema.sql +143 -0
- package/migrations/0002_add_columns.sql +12 -0
- package/migrations/0003_add_tables.sql +53 -0
- package/migrations/0004_seed_policies.sql +17 -0
- package/migrations/0005_add_instance_indexes.sql +9 -0
- package/migrations/0006_add_audit_query_indexes.sql +8 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth — Core auth handler for Cloudflare Workers.
|
|
3
|
+
*
|
|
4
|
+
* Routes (prefix-stripped, under /.well-known/service/api/passkey):
|
|
5
|
+
* GET /register — Generate registration challenge options
|
|
6
|
+
* POST /register — Verify registration credential, create user, issue JWT
|
|
7
|
+
* GET /auth — Generate authentication challenge options
|
|
8
|
+
* POST /auth — Verify authentication credential, issue JWT
|
|
9
|
+
*
|
|
10
|
+
* Session and logout are handled by auth-handler at /did/session and /did/logout.
|
|
11
|
+
*/
|
|
12
|
+
import { deriveAccessKeyId, isAccessKeyToken } from "../access/access-key-util.js";
|
|
13
|
+
import { checkLoginAccess } from "../access/login-access-check.js";
|
|
14
|
+
import { LOGIN_PROVIDER } from "../constants.js";
|
|
15
|
+
import { ensureInstanceMembership } from "../identity/auto-membership.js";
|
|
16
|
+
import { signJWT, verifyJWT } from "../identity/jwt.js";
|
|
17
|
+
import { derivePasskeyDID } from "../identity/passkey-did.js";
|
|
18
|
+
import { generateChallengeOptions, verifyAuthentication, verifyRegistration } from "../identity/webauthn.js";
|
|
19
|
+
import { detectLocale } from "../locale.js";
|
|
20
|
+
import { renderLoginPage } from "../pages/login-page.js";
|
|
21
|
+
const DEFAULT_COOKIE_NAME = "login_token";
|
|
22
|
+
const DEFAULT_EXPIRES_IN = 7 * 24 * 60 * 60; // 7 days
|
|
23
|
+
const MAX_NAME_LENGTH = 64;
|
|
24
|
+
/** Sanitize and validate user-provided name. */
|
|
25
|
+
function sanitizeName(raw) {
|
|
26
|
+
if (!raw)
|
|
27
|
+
return undefined;
|
|
28
|
+
// Strip control chars (C0 range + DEL), trim, limit length
|
|
29
|
+
let clean = "";
|
|
30
|
+
for (const ch of raw) {
|
|
31
|
+
const code = ch.charCodeAt(0);
|
|
32
|
+
if (code >= 0x20 && code !== 0x7f)
|
|
33
|
+
clean += ch;
|
|
34
|
+
}
|
|
35
|
+
clean = clean.trim().slice(0, MAX_NAME_LENGTH);
|
|
36
|
+
return clean || undefined;
|
|
37
|
+
}
|
|
38
|
+
function jsonResponse(data, status = 200, headers) {
|
|
39
|
+
return new Response(JSON.stringify(data), {
|
|
40
|
+
status,
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Cache-Control": "private, no-store",
|
|
44
|
+
...headers,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function errorResponse(message, status = 400) {
|
|
49
|
+
return jsonResponse({ error: message }, status);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Extract Bearer token from Authorization header and resolve access key caller.
|
|
53
|
+
* Returns null if no access key token or if validation fails.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolveAccessKeyCaller(request, store, instanceDid) {
|
|
56
|
+
const authHeader = request.headers.get("Authorization");
|
|
57
|
+
if (!authHeader)
|
|
58
|
+
return null;
|
|
59
|
+
const token = authHeader.replace(/^Bearer\s+/i, "").trim();
|
|
60
|
+
if (!isAccessKeyToken(token))
|
|
61
|
+
return null;
|
|
62
|
+
const accessKeyId = deriveAccessKeyId(token);
|
|
63
|
+
if (!accessKeyId)
|
|
64
|
+
return null;
|
|
65
|
+
const key = await store.getAccessKeyById(accessKeyId);
|
|
66
|
+
if (!key)
|
|
67
|
+
return null;
|
|
68
|
+
// Check expiration
|
|
69
|
+
if (key.expireAt && new Date(key.expireAt) < new Date())
|
|
70
|
+
return null;
|
|
71
|
+
// Instance ownership check: instance-scoped key must match request's instanceDid
|
|
72
|
+
if (key.instanceDid) {
|
|
73
|
+
// Key is scoped to a specific instance — must match
|
|
74
|
+
if (!instanceDid || key.instanceDid !== instanceDid) {
|
|
75
|
+
return null; // key belongs to a different instance or no instance context
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Check creator exists and is not blocked
|
|
79
|
+
const creator = await store.getUserByDid(key.createdBy);
|
|
80
|
+
if (!creator)
|
|
81
|
+
return null;
|
|
82
|
+
// Fire-and-forget: update lastUsedAt
|
|
83
|
+
store.refreshAccessKeyLastUsed(accessKeyId).catch(() => { });
|
|
84
|
+
return {
|
|
85
|
+
did: creator.did,
|
|
86
|
+
pk: creator.pk,
|
|
87
|
+
role: key.role,
|
|
88
|
+
displayName: creator.fullName ?? undefined,
|
|
89
|
+
blocked: !creator.approved,
|
|
90
|
+
accessKeyId,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export class Auth {
|
|
94
|
+
store;
|
|
95
|
+
jwtSecret;
|
|
96
|
+
rpName;
|
|
97
|
+
rpID;
|
|
98
|
+
jwtExpiresIn;
|
|
99
|
+
cookieName;
|
|
100
|
+
constructor(options) {
|
|
101
|
+
this.store = options.store;
|
|
102
|
+
this.jwtSecret = options.jwtSecret;
|
|
103
|
+
this.rpName = options.rpName;
|
|
104
|
+
this.rpID = options.rpID;
|
|
105
|
+
this.jwtExpiresIn = options.jwtExpiresIn ?? DEFAULT_EXPIRES_IN;
|
|
106
|
+
this.cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
107
|
+
}
|
|
108
|
+
/** Expose the store for sharing with TeamHandler. */
|
|
109
|
+
getStore() {
|
|
110
|
+
return this.store;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Handle auth API requests. Expects the prefix to be already stripped —
|
|
114
|
+
* internally matches /register (GET/POST) and /auth (GET/POST).
|
|
115
|
+
*/
|
|
116
|
+
async fetch(request, instanceDid) {
|
|
117
|
+
const url = new URL(request.url);
|
|
118
|
+
// Strip the well-known prefix to get internal route
|
|
119
|
+
const path = url.pathname.replace("/.well-known/service/api/passkey", "");
|
|
120
|
+
if (path === "/register" && request.method === "GET") {
|
|
121
|
+
return this.handleRegisterRequest(request, instanceDid);
|
|
122
|
+
}
|
|
123
|
+
if (path === "/register" && request.method === "POST") {
|
|
124
|
+
return this.handleRegisterResponse(request, instanceDid);
|
|
125
|
+
}
|
|
126
|
+
if (path === "/auth" && request.method === "GET") {
|
|
127
|
+
return this.handleAuthRequest(request);
|
|
128
|
+
}
|
|
129
|
+
if (path === "/auth" && request.method === "POST") {
|
|
130
|
+
return this.handleAuthResponse(request, instanceDid);
|
|
131
|
+
}
|
|
132
|
+
if (path === "/connect/register" && request.method === "POST") {
|
|
133
|
+
return this.handleConnectRegister(request);
|
|
134
|
+
}
|
|
135
|
+
if (path === "/connect/verify" && request.method === "POST") {
|
|
136
|
+
return this.handleConnectVerify(request, instanceDid);
|
|
137
|
+
}
|
|
138
|
+
if (path === "/disconnect" && request.method === "POST") {
|
|
139
|
+
return this.handlePasskeyDisconnect(request, instanceDid);
|
|
140
|
+
}
|
|
141
|
+
if (path === "/rename" && request.method === "POST") {
|
|
142
|
+
return this.handlePasskeyRename(request, instanceDid);
|
|
143
|
+
}
|
|
144
|
+
return errorResponse("Not found", 404);
|
|
145
|
+
}
|
|
146
|
+
/** Verify JWT from cookie — hot path, pure crypto, no D1. */
|
|
147
|
+
async verify(request) {
|
|
148
|
+
const cookie = request.headers.get("Cookie");
|
|
149
|
+
if (!cookie)
|
|
150
|
+
return null;
|
|
151
|
+
const token = this.extractCookie(cookie);
|
|
152
|
+
if (!token)
|
|
153
|
+
return null;
|
|
154
|
+
const payload = await verifyJWT(token, this.jwtSecret);
|
|
155
|
+
if (!payload?.did)
|
|
156
|
+
return null;
|
|
157
|
+
return {
|
|
158
|
+
did: payload.did,
|
|
159
|
+
pk: payload.pk ?? "",
|
|
160
|
+
displayName: (payload.fullName ?? payload.displayName),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Full verification: access key (Bearer) first, then JWT + DB check.
|
|
165
|
+
* Returns null if neither auth method succeeds, or if user is blocked/deleted.
|
|
166
|
+
*/
|
|
167
|
+
async verifyFull(request, instanceDid) {
|
|
168
|
+
// Try access key auth first (pass instanceDid for isolation check)
|
|
169
|
+
const akCaller = await this.verifyAccessKey(request, instanceDid);
|
|
170
|
+
if (akCaller)
|
|
171
|
+
return akCaller;
|
|
172
|
+
// Fall back to JWT auth
|
|
173
|
+
const caller = await this.verify(request);
|
|
174
|
+
if (!caller)
|
|
175
|
+
return null;
|
|
176
|
+
const user = await this.store.getUserByDid(caller.did);
|
|
177
|
+
if (!user?.approved)
|
|
178
|
+
return null;
|
|
179
|
+
// Prefer instance membership role over global users.role
|
|
180
|
+
let role = user.role ?? "guest";
|
|
181
|
+
if (instanceDid) {
|
|
182
|
+
const membership = await this.store.getMembership(caller.did, instanceDid);
|
|
183
|
+
if (membership?.role) {
|
|
184
|
+
role = membership.role;
|
|
185
|
+
}
|
|
186
|
+
else if (role !== "owner" && role !== "admin") {
|
|
187
|
+
// No membership + not system owner/admin → guest (consistent with RPC resolveIdentity)
|
|
188
|
+
role = "guest";
|
|
189
|
+
}
|
|
190
|
+
// System owner/admin punch-through: keep global role for platform management
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
...caller,
|
|
194
|
+
displayName: user.fullName ?? caller.displayName,
|
|
195
|
+
role,
|
|
196
|
+
avatar: `/.well-known/service/avatar/${caller.did}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Verify access key from Authorization: Bearer header.
|
|
201
|
+
* Returns CallerIdentity with the key's role, or null.
|
|
202
|
+
*/
|
|
203
|
+
async verifyAccessKey(request, instanceDid) {
|
|
204
|
+
const resolved = await resolveAccessKeyCaller(request, this.store, instanceDid);
|
|
205
|
+
if (!resolved || resolved.blocked)
|
|
206
|
+
return null;
|
|
207
|
+
return {
|
|
208
|
+
did: resolved.did,
|
|
209
|
+
pk: resolved.pk,
|
|
210
|
+
displayName: resolved.displayName,
|
|
211
|
+
role: resolved.role,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** Return the login page HTML, filtering methods by builtin-providers settings. */
|
|
215
|
+
async getLoginPage(instanceDid, overrides, request) {
|
|
216
|
+
// Read builtin-providers config to filter methods
|
|
217
|
+
let methods = overrides?.methods;
|
|
218
|
+
if (instanceDid) {
|
|
219
|
+
try {
|
|
220
|
+
const raw = await this.store.getSetting(instanceDid, "auth:builtin-providers");
|
|
221
|
+
if (raw) {
|
|
222
|
+
const config = JSON.parse(raw);
|
|
223
|
+
// Build methods based on what's enabled, keeping existing order
|
|
224
|
+
const methodMap = { passkey: "passkey", "did-connect": "wallet", email: "email" };
|
|
225
|
+
if (methods) {
|
|
226
|
+
methods = methods.filter((m) => {
|
|
227
|
+
const configKey = methodMap[m];
|
|
228
|
+
if (!configKey)
|
|
229
|
+
return true; // oauth and unknown methods pass through
|
|
230
|
+
const entry = config[configKey];
|
|
231
|
+
return !entry || entry.enabled !== false;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// On error, keep all methods enabled (safe degradation)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Federation: check if branding should come from Master
|
|
241
|
+
let brandingSourceDid = instanceDid;
|
|
242
|
+
if (instanceDid) {
|
|
243
|
+
const fedRole = await this.store.getSetting(instanceDid, "federation:role");
|
|
244
|
+
const fedEnabled = await this.store.getSetting(instanceDid, "federation:enabled");
|
|
245
|
+
const fedGroupId = await this.store.getSetting(instanceDid, "federation:groupId");
|
|
246
|
+
if (fedRole === "member" && fedEnabled === "true" && fedGroupId) {
|
|
247
|
+
brandingSourceDid = fedGroupId; // Master's instanceDid
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Read branding: appName from local instance, logo + theme from Master in federation
|
|
251
|
+
const isFederated = brandingSourceDid !== instanceDid;
|
|
252
|
+
let appName = this.rpName;
|
|
253
|
+
let masterAppName;
|
|
254
|
+
let appLogo;
|
|
255
|
+
let themePrefer;
|
|
256
|
+
// appName always from local instance (not Master) — users should see the sub-site's name
|
|
257
|
+
if (instanceDid) {
|
|
258
|
+
try {
|
|
259
|
+
const localBrandingRaw = await this.store.getSetting(instanceDid, "app:branding");
|
|
260
|
+
if (localBrandingRaw) {
|
|
261
|
+
const localBranding = JSON.parse(localBrandingRaw);
|
|
262
|
+
if (localBranding.name)
|
|
263
|
+
appName = localBranding.name;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch { /* keep rpName default */ }
|
|
267
|
+
}
|
|
268
|
+
// Logo + theme from brandingSourceDid (Master in federation); also read Master's name for hint
|
|
269
|
+
if (brandingSourceDid) {
|
|
270
|
+
try {
|
|
271
|
+
const [brandingRaw, logosRaw, themeRaw] = await Promise.all([
|
|
272
|
+
isFederated ? this.store.getSetting(brandingSourceDid, "app:branding") : Promise.resolve(null),
|
|
273
|
+
this.store.getSetting(brandingSourceDid, "app:logos"),
|
|
274
|
+
this.store.getSetting(brandingSourceDid, "app:theme"),
|
|
275
|
+
]);
|
|
276
|
+
if (isFederated && brandingRaw) {
|
|
277
|
+
const masterBranding = JSON.parse(brandingRaw);
|
|
278
|
+
if (masterBranding.name)
|
|
279
|
+
masterAppName = masterBranding.name;
|
|
280
|
+
}
|
|
281
|
+
if (logosRaw) {
|
|
282
|
+
const logos = JSON.parse(logosRaw);
|
|
283
|
+
if (logos.square) {
|
|
284
|
+
appLogo = overrides?.masterOAuthOrigin
|
|
285
|
+
? `${overrides.masterOAuthOrigin}/.well-known/service/blocklet/logo?t=${Date.now()}`
|
|
286
|
+
: `/.well-known/service/blocklet/logo?t=${Date.now()}`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (themeRaw) {
|
|
290
|
+
const theme = JSON.parse(themeRaw);
|
|
291
|
+
const concept = theme.concepts?.find((c) => c.id === theme.currentConceptId);
|
|
292
|
+
const prefer = concept?.prefer || theme.prefer;
|
|
293
|
+
if (prefer === "light" || prefer === "dark")
|
|
294
|
+
themePrefer = prefer;
|
|
295
|
+
else if (prefer === "system")
|
|
296
|
+
themePrefer = "auto";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// On error, use defaults
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Detect locale from request
|
|
304
|
+
const locale = request ? detectLocale(request) : "en";
|
|
305
|
+
const html = renderLoginPage({
|
|
306
|
+
apiPrefix: "/.well-known/service/api/did",
|
|
307
|
+
appName,
|
|
308
|
+
appLogo,
|
|
309
|
+
methods,
|
|
310
|
+
oauthProviders: overrides?.oauthProviders,
|
|
311
|
+
locale,
|
|
312
|
+
theme: themePrefer,
|
|
313
|
+
masterOAuthOrigin: overrides?.masterOAuthOrigin,
|
|
314
|
+
masterAppName,
|
|
315
|
+
});
|
|
316
|
+
return new Response(html, {
|
|
317
|
+
headers: {
|
|
318
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
319
|
+
"Cache-Control": "private, no-store",
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// ─── Internal route handlers ────────────────────────────────────────
|
|
324
|
+
/** GET /register — Generate registration challenge options.
|
|
325
|
+
* Always returns options (even when registration is gated) because:
|
|
326
|
+
* - Re-registration of existing passkeys bypasses the gate
|
|
327
|
+
* - The registrationAllowed flag lets the client show/hide UI
|
|
328
|
+
* - The actual gate is enforced at POST /register time */
|
|
329
|
+
async handleRegisterRequest(request, instanceDid) {
|
|
330
|
+
const rpID = this.getRPID(request);
|
|
331
|
+
const challengeId = crypto.randomUUID();
|
|
332
|
+
const challenge = crypto.randomUUID();
|
|
333
|
+
const url = new URL(request.url);
|
|
334
|
+
const userName = sanitizeName(url.searchParams.get("name") ?? undefined);
|
|
335
|
+
const invitationId = url.searchParams.get("invitationId") ?? undefined;
|
|
336
|
+
const eligibility = await this.checkRegistrationEligibility(invitationId, instanceDid);
|
|
337
|
+
// Audit blocked registration attempts at GET time (frontend won't POST)
|
|
338
|
+
if (!eligibility.allowed) {
|
|
339
|
+
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
340
|
+
const hostname = new URL(request.url).hostname;
|
|
341
|
+
this.store.createAuditLog({
|
|
342
|
+
action: "user.register_blocked",
|
|
343
|
+
operatorDid: "anonymous",
|
|
344
|
+
metadata: {
|
|
345
|
+
source: "passkey",
|
|
346
|
+
method: eligibility.method,
|
|
347
|
+
reason: eligibility.reason,
|
|
348
|
+
invitationId: invitationId ?? null,
|
|
349
|
+
domain: hostname,
|
|
350
|
+
},
|
|
351
|
+
ip,
|
|
352
|
+
instanceDid,
|
|
353
|
+
}).catch(() => { });
|
|
354
|
+
}
|
|
355
|
+
await this.store.saveChallenge(challengeId, challenge, invitationId);
|
|
356
|
+
this.store.purgeExpiredChallenges().catch(() => { });
|
|
357
|
+
const options = await generateChallengeOptions({
|
|
358
|
+
rpID,
|
|
359
|
+
rpName: this.rpName,
|
|
360
|
+
challenge,
|
|
361
|
+
userName,
|
|
362
|
+
});
|
|
363
|
+
// Return flattened WebAuthn options — Connect SDK passes entire response
|
|
364
|
+
// as optionsJSON to startRegistration({ optionsJSON }), reads .challenge directly.
|
|
365
|
+
// challengeId kept for our admin UI compatibility.
|
|
366
|
+
return jsonResponse({
|
|
367
|
+
...options.registration,
|
|
368
|
+
challengeId,
|
|
369
|
+
registrationAllowed: eligibility.allowed,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/** POST /register — Verify registration credential, create user, issue JWT.
|
|
373
|
+
* Two calling conventions:
|
|
374
|
+
* 1. Admin UI: body = { challengeId, credential, name? }
|
|
375
|
+
* 2. Connect SDK: body = WebAuthn credential directly, query ?challenge=<base64> */
|
|
376
|
+
async handleRegisterResponse(request, instanceDid) {
|
|
377
|
+
const url = new URL(request.url);
|
|
378
|
+
const body = (await request.json());
|
|
379
|
+
// Connect SDK sends credential as entire body, challenge in query param
|
|
380
|
+
const credential = body.credential || (body.id ? body : undefined);
|
|
381
|
+
if (!credential) {
|
|
382
|
+
return errorResponse("Missing required fields");
|
|
383
|
+
}
|
|
384
|
+
const userName = sanitizeName(body.name ?? undefined);
|
|
385
|
+
// Resolve challenge: try challengeId (admin UI) → query param challenge (SDK)
|
|
386
|
+
const stored = await this.resolveChallenge(body.challengeId, url.searchParams.get("challenge"));
|
|
387
|
+
if (!stored) {
|
|
388
|
+
return errorResponse("Invalid or expired challenge");
|
|
389
|
+
}
|
|
390
|
+
await this.store.deleteChallenge(stored.id);
|
|
391
|
+
const { challenge, invitationId } = stored;
|
|
392
|
+
const rpID = this.getRPID(request);
|
|
393
|
+
const requestUrl = new URL(request.url);
|
|
394
|
+
const origin = request.headers.get("Origin") || requestUrl.origin;
|
|
395
|
+
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
396
|
+
const hostname = requestUrl.hostname;
|
|
397
|
+
const cookieDomain = this.rpID ? rpID : undefined;
|
|
398
|
+
const ctx = {
|
|
399
|
+
expectedChallenge: challenge,
|
|
400
|
+
expectedOrigin: origin,
|
|
401
|
+
expectedRPID: rpID,
|
|
402
|
+
ip,
|
|
403
|
+
hostname,
|
|
404
|
+
cookieDomain,
|
|
405
|
+
request,
|
|
406
|
+
};
|
|
407
|
+
try {
|
|
408
|
+
return await this.handleRegistrationVerify(credential, ctx, userName, invitationId ?? undefined, instanceDid);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
const message = err instanceof Error ? err.message : "Registration verification failed";
|
|
412
|
+
return errorResponse(message);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** GET /auth — Generate authentication challenge options. */
|
|
416
|
+
async handleAuthRequest(request) {
|
|
417
|
+
const rpID = this.getRPID(request);
|
|
418
|
+
const challengeId = crypto.randomUUID();
|
|
419
|
+
const challenge = crypto.randomUUID();
|
|
420
|
+
await this.store.saveChallenge(challengeId, challenge);
|
|
421
|
+
this.store.purgeExpiredChallenges().catch(() => { });
|
|
422
|
+
const options = await generateChallengeOptions({
|
|
423
|
+
rpID,
|
|
424
|
+
rpName: this.rpName,
|
|
425
|
+
challenge,
|
|
426
|
+
});
|
|
427
|
+
// Return flattened WebAuthn options — Connect SDK passes entire response
|
|
428
|
+
// as optionsJSON to startAuthentication({ optionsJSON }), reads .challenge directly.
|
|
429
|
+
return jsonResponse({
|
|
430
|
+
...options.authentication,
|
|
431
|
+
challengeId,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
/** POST /auth — Verify authentication credential, issue JWT.
|
|
435
|
+
* Two calling conventions:
|
|
436
|
+
* 1. Admin UI: body = { challengeId, credential }
|
|
437
|
+
* 2. Connect SDK: body = WebAuthn credential directly, query ?challenge=<base64> */
|
|
438
|
+
async handleAuthResponse(request, instanceDid) {
|
|
439
|
+
const url = new URL(request.url);
|
|
440
|
+
const body = (await request.json());
|
|
441
|
+
const credential = body.credential || (body.id ? body : undefined);
|
|
442
|
+
if (!credential) {
|
|
443
|
+
return errorResponse("Missing required fields");
|
|
444
|
+
}
|
|
445
|
+
const stored = await this.resolveChallenge(body.challengeId, url.searchParams.get("challenge"));
|
|
446
|
+
if (!stored) {
|
|
447
|
+
return errorResponse("Invalid or expired challenge");
|
|
448
|
+
}
|
|
449
|
+
await this.store.deleteChallenge(stored.id);
|
|
450
|
+
const { challenge } = stored;
|
|
451
|
+
const rpID = this.getRPID(request);
|
|
452
|
+
const requestUrl = new URL(request.url);
|
|
453
|
+
const origin = request.headers.get("Origin") || requestUrl.origin;
|
|
454
|
+
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
455
|
+
const hostname = requestUrl.hostname;
|
|
456
|
+
const cookieDomain = this.rpID ? rpID : undefined;
|
|
457
|
+
const ctx = {
|
|
458
|
+
expectedChallenge: challenge,
|
|
459
|
+
expectedOrigin: origin,
|
|
460
|
+
expectedRPID: rpID,
|
|
461
|
+
ip,
|
|
462
|
+
hostname,
|
|
463
|
+
cookieDomain,
|
|
464
|
+
request,
|
|
465
|
+
};
|
|
466
|
+
try {
|
|
467
|
+
return await this.handleAuthenticationVerify(credential, ctx, instanceDid);
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
const message = err instanceof Error ? err.message : "Authentication verification failed";
|
|
471
|
+
return errorResponse(message);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async handleRegistrationVerify(credential, ctx, userName, invitationId, instanceDid) {
|
|
475
|
+
const { expectedChallenge, expectedOrigin, expectedRPID, ip, hostname, cookieDomain, request } = ctx;
|
|
476
|
+
const verification = await verifyRegistration({
|
|
477
|
+
response: credential,
|
|
478
|
+
expectedChallenge,
|
|
479
|
+
expectedOrigin,
|
|
480
|
+
expectedRPID,
|
|
481
|
+
});
|
|
482
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
483
|
+
return errorResponse("Registration verification failed");
|
|
484
|
+
}
|
|
485
|
+
const { credential: regCredential, credentialDeviceType, credentialBackedUp, } = verification.registrationInfo;
|
|
486
|
+
const publicKeyBytes = regCredential.publicKey;
|
|
487
|
+
const credentialId = regCredential.id;
|
|
488
|
+
// Derive DID from passkey public key
|
|
489
|
+
const passkeyDid = derivePasskeyDID(publicKeyBytes);
|
|
490
|
+
const pkBase64 = uint8ArrayToBase64(publicKeyBytes);
|
|
491
|
+
// Check if this passkey already exists (re-registration)
|
|
492
|
+
const existing = await this.store.getConnectedAccountByDid(passkeyDid);
|
|
493
|
+
if (existing) {
|
|
494
|
+
// Treat as login for existing passkey
|
|
495
|
+
await this.store.updateLastLogin(existing.userDid, ip, hostname);
|
|
496
|
+
await ensureInstanceMembership(this.store, existing.userDid, instanceDid);
|
|
497
|
+
const user = await this.store.getUserByDid(existing.userDid);
|
|
498
|
+
// Audit log: re-registration login
|
|
499
|
+
await this.store.createAuditLog({
|
|
500
|
+
action: "user.login",
|
|
501
|
+
operatorDid: existing.userDid,
|
|
502
|
+
metadata: { source: "passkey", type: "re-registration", domain: hostname },
|
|
503
|
+
ip,
|
|
504
|
+
instanceDid,
|
|
505
|
+
});
|
|
506
|
+
return this.issueJWT(existing.userDid, existing.pk ?? pkBase64, user?.fullName ?? undefined, user?.role ?? undefined, cookieDomain, request, "passkey", instanceDid);
|
|
507
|
+
}
|
|
508
|
+
// Server-side registration gate (L2 defense)
|
|
509
|
+
const eligibility = await this.checkRegistrationEligibility(invitationId, instanceDid);
|
|
510
|
+
if (!eligibility.allowed) {
|
|
511
|
+
await this.store.createAuditLog({
|
|
512
|
+
action: "user.register_blocked",
|
|
513
|
+
operatorDid: passkeyDid,
|
|
514
|
+
metadata: {
|
|
515
|
+
source: "passkey",
|
|
516
|
+
method: eligibility.method,
|
|
517
|
+
reason: eligibility.reason,
|
|
518
|
+
invitationId: invitationId ?? null,
|
|
519
|
+
domain: hostname,
|
|
520
|
+
},
|
|
521
|
+
ip,
|
|
522
|
+
instanceDid,
|
|
523
|
+
});
|
|
524
|
+
return errorResponse("Registration is not allowed. An invitation is required.", 403);
|
|
525
|
+
}
|
|
526
|
+
// New registration: user DID = first passkey DID
|
|
527
|
+
const userDid = passkeyDid;
|
|
528
|
+
await this.store.createUser({
|
|
529
|
+
did: userDid,
|
|
530
|
+
pk: pkBase64,
|
|
531
|
+
fullName: userName,
|
|
532
|
+
sourceProvider: LOGIN_PROVIDER.PASSKEY,
|
|
533
|
+
ip,
|
|
534
|
+
domain: hostname,
|
|
535
|
+
});
|
|
536
|
+
const transports = credential?.response
|
|
537
|
+
?.transports;
|
|
538
|
+
await this.store.createConnectedAccount({
|
|
539
|
+
did: passkeyDid,
|
|
540
|
+
pk: pkBase64,
|
|
541
|
+
userDid,
|
|
542
|
+
provider: "passkey",
|
|
543
|
+
id: credentialId,
|
|
544
|
+
extra: JSON.stringify({
|
|
545
|
+
transports,
|
|
546
|
+
rpID: expectedRPID,
|
|
547
|
+
credentialDeviceType,
|
|
548
|
+
credentialBackedUp,
|
|
549
|
+
}),
|
|
550
|
+
userInfo: JSON.stringify({ name: userName || "Passkey User" }),
|
|
551
|
+
ip,
|
|
552
|
+
});
|
|
553
|
+
await this.store.incrementPasskeyCount(userDid);
|
|
554
|
+
// Resolve the role for this new registration
|
|
555
|
+
// Priority: first-user → invitation role → instance-first-owner → "member"
|
|
556
|
+
const userCount = await this.store.getUserCount();
|
|
557
|
+
let role;
|
|
558
|
+
// Resolve invitation role (do NOT consume here — the invite page's
|
|
559
|
+
// POST /accept endpoint handles consumption atomically)
|
|
560
|
+
let invitedRole;
|
|
561
|
+
if (invitationId) {
|
|
562
|
+
const invitation = await this.store.getInvitation(invitationId);
|
|
563
|
+
if (invitation && invitation.status === "active" && invitation.useCount < invitation.maxUses) {
|
|
564
|
+
invitedRole = invitation.role;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (userCount === 1) {
|
|
568
|
+
// First-user-is-owner: global first user gets global owner role
|
|
569
|
+
await this.store.updateUserRole(userDid, "owner");
|
|
570
|
+
role = "owner";
|
|
571
|
+
}
|
|
572
|
+
if (instanceDid) {
|
|
573
|
+
const members = await this.store.listMemberships(instanceDid);
|
|
574
|
+
const hasOwner = members.some((m) => m.role === "owner");
|
|
575
|
+
// Instance role: first-owner > invitation role > "guest"
|
|
576
|
+
const instanceRole = hasOwner ? (invitedRole ?? role ?? "guest") : "owner";
|
|
577
|
+
const existing = await this.store.getMembership(userDid, instanceDid);
|
|
578
|
+
if (!existing) {
|
|
579
|
+
await this.store.createMembership(userDid, instanceDid, instanceRole);
|
|
580
|
+
}
|
|
581
|
+
if (!role)
|
|
582
|
+
role = instanceRole;
|
|
583
|
+
}
|
|
584
|
+
else if (invitedRole && !role) {
|
|
585
|
+
// System-level invitation: set global user role
|
|
586
|
+
await this.store.updateUserRole(userDid, invitedRole);
|
|
587
|
+
role = invitedRole;
|
|
588
|
+
}
|
|
589
|
+
// Audit log: new registration
|
|
590
|
+
await this.store.createAuditLog({
|
|
591
|
+
action: "user.register",
|
|
592
|
+
operatorDid: userDid,
|
|
593
|
+
metadata: { role: role ?? "guest", source: "passkey", domain: hostname },
|
|
594
|
+
ip,
|
|
595
|
+
instanceDid,
|
|
596
|
+
});
|
|
597
|
+
return this.issueJWT(userDid, pkBase64, userName, role, cookieDomain, request, "passkey", instanceDid);
|
|
598
|
+
}
|
|
599
|
+
async handleAuthenticationVerify(credential, ctx, instanceDid) {
|
|
600
|
+
const { expectedChallenge, expectedOrigin, expectedRPID, ip, hostname, cookieDomain, request } = ctx;
|
|
601
|
+
const credentialData = credential;
|
|
602
|
+
const credentialId = credentialData.id;
|
|
603
|
+
// Look up stored passkey by credential ID
|
|
604
|
+
const stored = await this.store.getConnectedAccountById(credentialId);
|
|
605
|
+
if (!stored?.extra) {
|
|
606
|
+
return errorResponse("Unknown credential");
|
|
607
|
+
}
|
|
608
|
+
const extra = JSON.parse(stored.extra);
|
|
609
|
+
// Reconstruct public key from base64
|
|
610
|
+
const publicKey = base64ToUint8Array(stored.pk ?? "");
|
|
611
|
+
const verification = await verifyAuthentication({
|
|
612
|
+
response: credential,
|
|
613
|
+
expectedChallenge,
|
|
614
|
+
expectedOrigin,
|
|
615
|
+
expectedRPID,
|
|
616
|
+
credential: {
|
|
617
|
+
id: credentialId,
|
|
618
|
+
publicKey,
|
|
619
|
+
counter: stored.counter,
|
|
620
|
+
transports: extra.transports,
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
if (!verification.verified) {
|
|
624
|
+
// Audit log: failed authentication. operatorDid is the user who owns the credential.
|
|
625
|
+
// Note: error reason is not recorded to avoid leaking internal details to log readers.
|
|
626
|
+
await this.store.createAuditLog({
|
|
627
|
+
action: "user.login_failed",
|
|
628
|
+
operatorDid: stored.userDid,
|
|
629
|
+
metadata: { source: "passkey", credentialId, domain: hostname },
|
|
630
|
+
ip,
|
|
631
|
+
instanceDid,
|
|
632
|
+
});
|
|
633
|
+
return errorResponse("Authentication verification failed");
|
|
634
|
+
}
|
|
635
|
+
// Update counter and last login
|
|
636
|
+
await this.store.updateCounter(stored.did, verification.authenticationInfo.newCounter);
|
|
637
|
+
await this.store.updateLastLogin(stored.userDid, ip, hostname);
|
|
638
|
+
await ensureInstanceMembership(this.store, stored.userDid, instanceDid);
|
|
639
|
+
const user = await this.store.getUserByDid(stored.userDid);
|
|
640
|
+
// Resolve role: prefer instance membership role over global users.role
|
|
641
|
+
let role = user?.role ?? "guest";
|
|
642
|
+
if (instanceDid) {
|
|
643
|
+
const membership = await this.store.getMembership(stored.userDid, instanceDid);
|
|
644
|
+
if (membership?.role) {
|
|
645
|
+
role = membership.role;
|
|
646
|
+
}
|
|
647
|
+
else if (role !== "owner" && role !== "admin") {
|
|
648
|
+
role = "guest";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Audit log: login. We probe checkLoginAccess here to populate accessBlocked
|
|
652
|
+
// metadata. The actual access enforcement happens inside issueJWT() below
|
|
653
|
+
// (this double-call is intentional and cheap; the helper is read-only).
|
|
654
|
+
const access = await checkLoginAccess(this.store, instanceDid, role);
|
|
655
|
+
await this.store.createAuditLog({
|
|
656
|
+
action: "user.login",
|
|
657
|
+
operatorDid: stored.userDid,
|
|
658
|
+
metadata: {
|
|
659
|
+
source: "passkey",
|
|
660
|
+
domain: hostname,
|
|
661
|
+
accessBlocked: !access.allowed,
|
|
662
|
+
blockReason: access.allowed ? undefined : access.reason,
|
|
663
|
+
},
|
|
664
|
+
ip,
|
|
665
|
+
instanceDid,
|
|
666
|
+
});
|
|
667
|
+
return this.issueJWT(stored.userDid, stored.pk ?? "", user?.fullName ?? undefined, role, cookieDomain, request, "passkey", instanceDid);
|
|
668
|
+
}
|
|
669
|
+
/** Clear auth cookie. GET → redirect to return_to or Referer, POST → JSON response. */
|
|
670
|
+
logout(request) {
|
|
671
|
+
// Fire-and-forget: write user.logout audit log without blocking response
|
|
672
|
+
this.verify(request).then((identity) => {
|
|
673
|
+
if (identity?.did) {
|
|
674
|
+
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
675
|
+
this.store.createAuditLog({
|
|
676
|
+
action: "user.logout",
|
|
677
|
+
operatorDid: identity.did,
|
|
678
|
+
ip,
|
|
679
|
+
}).catch(() => { });
|
|
680
|
+
}
|
|
681
|
+
}).catch(() => { });
|
|
682
|
+
const cookieDomain = this.rpID ? this.getRPID(request) : undefined;
|
|
683
|
+
const isSecure = new URL(request.url).protocol === "https:";
|
|
684
|
+
const securePart = isSecure ? " Secure;" : "";
|
|
685
|
+
let clearCookie = `${this.cookieName}=; Path=/;${securePart} SameSite=Lax; Max-Age=0`;
|
|
686
|
+
if (cookieDomain?.includes("."))
|
|
687
|
+
clearCookie += `; Domain=${cookieDomain}`;
|
|
688
|
+
if (request.method === "GET") {
|
|
689
|
+
const url = new URL(request.url);
|
|
690
|
+
const returnTo = url.searchParams.get("return_to") || request.headers.get("Referer") || "/";
|
|
691
|
+
// Only allow same-origin redirects to prevent open redirect (reject protocol-relative //evil.com)
|
|
692
|
+
let redirectUrl = "/";
|
|
693
|
+
try {
|
|
694
|
+
const parsed = new URL(returnTo, url.origin);
|
|
695
|
+
if (parsed.origin === url.origin)
|
|
696
|
+
redirectUrl = parsed.pathname + parsed.search + parsed.hash;
|
|
697
|
+
}
|
|
698
|
+
catch { /* invalid URL → fallback to / */ }
|
|
699
|
+
return new Response(null, {
|
|
700
|
+
status: 302,
|
|
701
|
+
headers: {
|
|
702
|
+
Location: redirectUrl,
|
|
703
|
+
"Set-Cookie": clearCookie,
|
|
704
|
+
"Cache-Control": "private, no-store",
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return jsonResponse({ ok: true }, 200, { "Set-Cookie": clearCookie });
|
|
709
|
+
}
|
|
710
|
+
// ─── Registration eligibility ──────────────────────────────────────
|
|
711
|
+
/**
|
|
712
|
+
* Determine if passkey registration should be allowed.
|
|
713
|
+
*
|
|
714
|
+
* Returns `{ allowed: true }` if ANY of:
|
|
715
|
+
* 1. No users exist (first user becomes owner)
|
|
716
|
+
* 2. Default access policy is "public"
|
|
717
|
+
* 3. A valid invitation is presented
|
|
718
|
+
*
|
|
719
|
+
* Otherwise returns `{ allowed: false, reason, method }` for audit logging.
|
|
720
|
+
*/
|
|
721
|
+
async checkRegistrationEligibility(invitationId, instanceDid) {
|
|
722
|
+
const userCount = await this.store.getUserCount();
|
|
723
|
+
if (userCount === 0)
|
|
724
|
+
return { allowed: true };
|
|
725
|
+
const isOpen = await this.store.isRegistrationOpen(instanceDid);
|
|
726
|
+
if (isOpen)
|
|
727
|
+
return { allowed: true };
|
|
728
|
+
if (invitationId) {
|
|
729
|
+
const invitation = await this.store.getInvitation(invitationId);
|
|
730
|
+
if (!invitation) {
|
|
731
|
+
return { allowed: false, reason: "invitation_not_found", method: "invite" };
|
|
732
|
+
}
|
|
733
|
+
if (invitation.status !== "active") {
|
|
734
|
+
return { allowed: false, reason: `invitation_${invitation.status}`, method: "invite" };
|
|
735
|
+
}
|
|
736
|
+
if (new Date(invitation.expireAt) <= new Date()) {
|
|
737
|
+
return { allowed: false, reason: "invitation_expired", method: "invite" };
|
|
738
|
+
}
|
|
739
|
+
if (invitation.useCount >= invitation.maxUses) {
|
|
740
|
+
return { allowed: false, reason: "invitation_exhausted", method: "invite" };
|
|
741
|
+
}
|
|
742
|
+
// Valid invitation
|
|
743
|
+
return { allowed: true };
|
|
744
|
+
}
|
|
745
|
+
return { allowed: false, reason: "registration_closed", method: "direct" };
|
|
746
|
+
}
|
|
747
|
+
// ─── Passkey Connect/Disconnect (for logged-in users) ───────────────
|
|
748
|
+
/** POST /connect/register — generate registration options for adding a passkey to an existing account. */
|
|
749
|
+
async handleConnectRegister(request) {
|
|
750
|
+
const caller = await this.verifyFull(request);
|
|
751
|
+
if (!caller)
|
|
752
|
+
return errorResponse("Authentication required", 401);
|
|
753
|
+
const rpID = this.getRPID(request);
|
|
754
|
+
const challengeId = crypto.randomUUID();
|
|
755
|
+
const challenge = crypto.randomUUID();
|
|
756
|
+
await this.store.saveChallenge(challengeId, challenge);
|
|
757
|
+
this.store.purgeExpiredChallenges().catch(() => { });
|
|
758
|
+
// Exclude existing passkey credential IDs to prevent re-registration
|
|
759
|
+
const existingPasskeys = await this.store.getConnectedAccountsByProviderAndUser("passkey", caller.did);
|
|
760
|
+
const excludeCredentials = existingPasskeys
|
|
761
|
+
.filter((p) => p.id != null)
|
|
762
|
+
.map((p) => ({
|
|
763
|
+
id: p.id,
|
|
764
|
+
type: "public-key",
|
|
765
|
+
}));
|
|
766
|
+
const options = await generateChallengeOptions({
|
|
767
|
+
rpID,
|
|
768
|
+
rpName: this.rpName,
|
|
769
|
+
challenge,
|
|
770
|
+
excludeCredentials,
|
|
771
|
+
});
|
|
772
|
+
return jsonResponse({
|
|
773
|
+
...options.registration,
|
|
774
|
+
challengeId,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
/** POST /connect/verify — verify registration credential and bind passkey to existing user. */
|
|
778
|
+
async handleConnectVerify(request, instanceDid) {
|
|
779
|
+
const caller = await this.verifyFull(request);
|
|
780
|
+
if (!caller)
|
|
781
|
+
return errorResponse("Authentication required", 401);
|
|
782
|
+
const body = (await request.json());
|
|
783
|
+
const { challengeId, credential } = body;
|
|
784
|
+
if (!challengeId || !credential) {
|
|
785
|
+
return errorResponse("Missing required fields");
|
|
786
|
+
}
|
|
787
|
+
const userName = sanitizeName(body.name);
|
|
788
|
+
const stored = await this.store.getChallenge(challengeId);
|
|
789
|
+
if (!stored) {
|
|
790
|
+
return errorResponse("Invalid or expired challenge");
|
|
791
|
+
}
|
|
792
|
+
await this.store.deleteChallenge(challengeId);
|
|
793
|
+
const { challenge } = stored;
|
|
794
|
+
const rpID = this.getRPID(request);
|
|
795
|
+
const requestUrl = new URL(request.url);
|
|
796
|
+
const origin = request.headers.get("Origin") || requestUrl.origin;
|
|
797
|
+
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
798
|
+
const hostname = requestUrl.hostname;
|
|
799
|
+
try {
|
|
800
|
+
const verification = await verifyRegistration({
|
|
801
|
+
response: credential,
|
|
802
|
+
expectedChallenge: challenge,
|
|
803
|
+
expectedOrigin: origin,
|
|
804
|
+
expectedRPID: rpID,
|
|
805
|
+
});
|
|
806
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
807
|
+
return errorResponse("Registration verification failed");
|
|
808
|
+
}
|
|
809
|
+
const { credential: regCredential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
|
810
|
+
const credentialId = regCredential.id;
|
|
811
|
+
const publicKeyBytes = regCredential.publicKey;
|
|
812
|
+
const pkBase64 = uint8ArrayToBase64(publicKeyBytes);
|
|
813
|
+
const passkeyDid = derivePasskeyDID(publicKeyBytes);
|
|
814
|
+
const transports = credential?.response
|
|
815
|
+
?.transports;
|
|
816
|
+
// Bind to existing user — do NOT create a new user
|
|
817
|
+
await this.store.createConnectedAccount({
|
|
818
|
+
did: passkeyDid,
|
|
819
|
+
pk: pkBase64,
|
|
820
|
+
userDid: caller.did,
|
|
821
|
+
provider: "passkey",
|
|
822
|
+
id: credentialId,
|
|
823
|
+
extra: JSON.stringify({
|
|
824
|
+
transports,
|
|
825
|
+
rpID,
|
|
826
|
+
credentialDeviceType,
|
|
827
|
+
credentialBackedUp,
|
|
828
|
+
}),
|
|
829
|
+
userInfo: JSON.stringify({ name: userName || "Passkey" }),
|
|
830
|
+
ip,
|
|
831
|
+
});
|
|
832
|
+
await this.store.incrementPasskeyCount(caller.did);
|
|
833
|
+
await this.store.createAuditLog({
|
|
834
|
+
action: "passkey.connect",
|
|
835
|
+
operatorDid: caller.did,
|
|
836
|
+
metadata: { passkeyDid, domain: hostname },
|
|
837
|
+
ip,
|
|
838
|
+
instanceDid,
|
|
839
|
+
});
|
|
840
|
+
return jsonResponse({ ok: true, did: passkeyDid });
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
const message = err instanceof Error ? err.message : "Registration verification failed";
|
|
844
|
+
return errorResponse(message);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/** POST /disconnect — remove a passkey from the current user. */
|
|
848
|
+
async handlePasskeyDisconnect(request, instanceDid) {
|
|
849
|
+
const caller = await this.verifyFull(request);
|
|
850
|
+
if (!caller)
|
|
851
|
+
return errorResponse("Authentication required", 401);
|
|
852
|
+
let body;
|
|
853
|
+
try {
|
|
854
|
+
body = await request.json();
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
return errorResponse("Invalid request body");
|
|
858
|
+
}
|
|
859
|
+
const passkeyDid = body?.did;
|
|
860
|
+
if (!passkeyDid)
|
|
861
|
+
return errorResponse("Missing passkey DID");
|
|
862
|
+
// Verify this passkey belongs to the current user
|
|
863
|
+
const account = await this.store.getConnectedAccountByDid(passkeyDid);
|
|
864
|
+
if (!account || account.userDid !== caller.did) {
|
|
865
|
+
return errorResponse("Passkey not found", 404);
|
|
866
|
+
}
|
|
867
|
+
// sourceProvider protection: cannot remove last passkey if registered via passkey
|
|
868
|
+
const user = await this.store.getUserByDid(caller.did);
|
|
869
|
+
if (user?.sourceProvider === "passkey") {
|
|
870
|
+
const allPasskeys = await this.store.getConnectedAccountsByProviderAndUser("passkey", caller.did);
|
|
871
|
+
if (allPasskeys.length <= 1) {
|
|
872
|
+
return errorResponse("Cannot remove your last passkey (registration provider)", 403);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
await this.store.deleteConnectedAccount(passkeyDid);
|
|
876
|
+
await this.store.decrementPasskeyCount(caller.did);
|
|
877
|
+
const ip = request.headers.get("CF-Connecting-IP") ?? undefined;
|
|
878
|
+
await this.store.createAuditLog({
|
|
879
|
+
action: "passkey.disconnect",
|
|
880
|
+
operatorDid: caller.did,
|
|
881
|
+
metadata: { passkeyDid },
|
|
882
|
+
ip,
|
|
883
|
+
instanceDid,
|
|
884
|
+
});
|
|
885
|
+
return jsonResponse({ ok: true });
|
|
886
|
+
}
|
|
887
|
+
/** POST /rename — rename a passkey belonging to the current user. */
|
|
888
|
+
async handlePasskeyRename(request, instanceDid) {
|
|
889
|
+
const caller = await this.verifyFull(request);
|
|
890
|
+
if (!caller)
|
|
891
|
+
return errorResponse("Authentication required", 401);
|
|
892
|
+
let body;
|
|
893
|
+
try {
|
|
894
|
+
body = await request.json();
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
return errorResponse("Invalid request body");
|
|
898
|
+
}
|
|
899
|
+
const passkeyDid = body?.did;
|
|
900
|
+
const newName = sanitizeName(body?.name);
|
|
901
|
+
if (!passkeyDid)
|
|
902
|
+
return errorResponse("Missing passkey DID");
|
|
903
|
+
if (!newName)
|
|
904
|
+
return errorResponse("Missing name");
|
|
905
|
+
const account = await this.store.getConnectedAccountByDid(passkeyDid);
|
|
906
|
+
if (!account || account.userDid !== caller.did) {
|
|
907
|
+
return errorResponse("Passkey not found", 404);
|
|
908
|
+
}
|
|
909
|
+
await this.store.updateConnectedAccountName(passkeyDid, newName);
|
|
910
|
+
await this.store.createAuditLog({
|
|
911
|
+
action: "passkey.rename",
|
|
912
|
+
operatorDid: caller.did,
|
|
913
|
+
metadata: { passkeyDid, newName },
|
|
914
|
+
ip: request.headers.get("CF-Connecting-IP") ?? undefined,
|
|
915
|
+
instanceDid,
|
|
916
|
+
});
|
|
917
|
+
return jsonResponse({ ok: true });
|
|
918
|
+
}
|
|
919
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
920
|
+
async issueJWT(did, pk, displayName, role, cookieDomain, request, provider = "passkey", instanceDid) {
|
|
921
|
+
// Check access rules before issuing JWT
|
|
922
|
+
const access = await checkLoginAccess(this.store, instanceDid, role ?? "guest");
|
|
923
|
+
if (!access.allowed) {
|
|
924
|
+
return jsonResponse({ ok: false, error: access.reason, code: "FORBIDDEN" }, 403);
|
|
925
|
+
}
|
|
926
|
+
const payload = {
|
|
927
|
+
type: "user",
|
|
928
|
+
did,
|
|
929
|
+
pk,
|
|
930
|
+
role: role ?? "guest",
|
|
931
|
+
provider,
|
|
932
|
+
kyc: 0,
|
|
933
|
+
elevated: false,
|
|
934
|
+
};
|
|
935
|
+
if (displayName)
|
|
936
|
+
payload.fullName = displayName;
|
|
937
|
+
const token = await signJWT(payload, this.jwtSecret, this.jwtExpiresIn);
|
|
938
|
+
const maxAge = this.jwtExpiresIn;
|
|
939
|
+
const isSecure = !request || new URL(request.url).protocol === "https:";
|
|
940
|
+
const securePart = isSecure ? " Secure;" : "";
|
|
941
|
+
let cookie = `${this.cookieName}=${token}; Path=/;${securePart} SameSite=Lax; Max-Age=${maxAge}`;
|
|
942
|
+
if (cookieDomain?.includes("."))
|
|
943
|
+
cookie += `; Domain=${cookieDomain}`;
|
|
944
|
+
return jsonResponse({ ok: true, verified: true, sessionToken: token, refreshToken: token, did }, 200, { "Set-Cookie": cookie });
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Resolve challenge from either challengeId (admin UI) or base64-encoded challenge value (Connect SDK).
|
|
948
|
+
* Connect SDK sends ?challenge=<base64url(uuid)>, BS stores challenge as raw UUID.
|
|
949
|
+
*/
|
|
950
|
+
async resolveChallenge(challengeId, challengeParam) {
|
|
951
|
+
// 1. Try by challengeId (our admin UI sends this)
|
|
952
|
+
if (challengeId) {
|
|
953
|
+
const stored = await this.store.getChallenge(challengeId);
|
|
954
|
+
if (stored)
|
|
955
|
+
return { id: challengeId, ...stored };
|
|
956
|
+
}
|
|
957
|
+
// 2. Try by challenge value from query param (Connect SDK)
|
|
958
|
+
if (challengeParam) {
|
|
959
|
+
// SDK sends base64url-encoded challenge — try decoding
|
|
960
|
+
let decoded = challengeParam;
|
|
961
|
+
try {
|
|
962
|
+
const padded = challengeParam.replace(/-/g, "+").replace(/_/g, "/");
|
|
963
|
+
decoded = atob(padded);
|
|
964
|
+
}
|
|
965
|
+
catch { /* not valid base64, use as-is */ }
|
|
966
|
+
const row = await this.store.getChallengeByValue(decoded);
|
|
967
|
+
if (row)
|
|
968
|
+
return row;
|
|
969
|
+
// Also try the raw (non-decoded) value
|
|
970
|
+
if (decoded !== challengeParam) {
|
|
971
|
+
const row2 = await this.store.getChallengeByValue(challengeParam);
|
|
972
|
+
if (row2)
|
|
973
|
+
return row2;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
getRPID(request) {
|
|
979
|
+
if (typeof this.rpID === "function")
|
|
980
|
+
return this.rpID(request);
|
|
981
|
+
if (this.rpID) {
|
|
982
|
+
// WebAuthn RP IDs must not include a port number.
|
|
983
|
+
// X-Arc-Domain may arrive as "localhost:8787" in local dev — strip the port.
|
|
984
|
+
try {
|
|
985
|
+
return new URL(`https://${this.rpID}`).hostname;
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
return this.rpID;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return new URL(request.url).hostname;
|
|
992
|
+
}
|
|
993
|
+
extractCookie(cookieHeader) {
|
|
994
|
+
const prefix = `${this.cookieName}=`;
|
|
995
|
+
const cookies = cookieHeader.split(";");
|
|
996
|
+
for (const cookie of cookies) {
|
|
997
|
+
const trimmed = cookie.trim();
|
|
998
|
+
if (trimmed.startsWith(prefix)) {
|
|
999
|
+
return trimmed.slice(prefix.length);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function uint8ArrayToBase64(bytes) {
|
|
1006
|
+
let binary = "";
|
|
1007
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1008
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1009
|
+
}
|
|
1010
|
+
return btoa(binary);
|
|
1011
|
+
}
|
|
1012
|
+
function base64ToUint8Array(base64) {
|
|
1013
|
+
const binary = atob(base64);
|
|
1014
|
+
const bytes = new Uint8Array(binary.length);
|
|
1015
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1016
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1017
|
+
}
|
|
1018
|
+
return bytes;
|
|
1019
|
+
}
|
|
1020
|
+
//# sourceMappingURL=passkey-handler.js.map
|