@openlup/platform 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +350 -3
- package/dist/identity/adminAuthBinding.d.ts +28 -0
- package/dist/identity/adminAuthBinding.js +66 -0
- package/dist/identity/adminAuthorization.d.ts +34 -0
- package/dist/identity/adminAuthorization.js +72 -0
- package/dist/identity/adminContracts.d.ts +70 -0
- package/dist/identity/adminContracts.js +43 -0
- package/dist/identity/adminMagicLinkHandler.d.ts +39 -0
- package/dist/identity/adminMagicLinkHandler.js +94 -0
- package/dist/identity/adminMeHandler.d.ts +15 -0
- package/dist/identity/adminMeHandler.js +49 -0
- package/dist/identity/adminPreflight.d.ts +16 -0
- package/dist/identity/adminPreflight.js +38 -0
- package/dist/identity/adminUserActionPorts.d.ts +31 -0
- package/dist/identity/adminUserActionPorts.js +51 -0
- package/dist/identity/adminUserHandlers.d.ts +38 -0
- package/dist/identity/adminUserHandlers.js +140 -0
- package/dist/identity/authVerifyUrl.d.ts +6 -0
- package/dist/identity/authVerifyUrl.js +54 -0
- package/dist/identity/contracts/contracts.d.ts +8 -0
- package/dist/identity/contracts/contracts.js +2 -0
- package/dist/identity/contracts/emailTrust.d.ts +3 -0
- package/dist/identity/contracts/emailTrust.js +4 -0
- package/dist/identity/contracts/index.d.ts +8 -0
- package/dist/identity/contracts/index.js +8 -0
- package/dist/identity/contracts/oidc.d.ts +3 -0
- package/dist/identity/contracts/oidc.js +9 -0
- package/dist/identity/contracts/operatorAuth.d.ts +35 -0
- package/dist/identity/contracts/operatorAuth.js +53 -0
- package/dist/identity/contracts/operatorInvitations.d.ts +37 -0
- package/dist/identity/contracts/operatorInvitations.js +1 -0
- package/dist/identity/contracts/ports.d.ts +22 -0
- package/dist/identity/contracts/ports.js +1 -0
- package/dist/identity/contracts/privacy.d.ts +62 -0
- package/dist/identity/contracts/privacy.js +99 -0
- package/dist/identity/contracts/providerList.d.ts +3 -0
- package/dist/identity/contracts/providerList.js +11 -0
- package/dist/identity/contracts/types.d.ts +16 -0
- package/dist/identity/contracts/types.js +4 -0
- package/dist/identity/contribution.d.ts +8 -0
- package/dist/identity/contribution.js +22 -0
- package/dist/identity/customerAuth.d.ts +8 -0
- package/dist/identity/customerAuth.js +1 -0
- package/dist/identity/customerAuthorization.d.ts +5 -0
- package/dist/identity/customerAuthorization.js +22 -0
- package/dist/identity/customerContracts.d.ts +42 -0
- package/dist/identity/customerContracts.js +43 -0
- package/dist/identity/customerIdentityBinding.d.ts +44 -0
- package/dist/identity/customerIdentityBinding.js +60 -0
- package/dist/identity/customerIdentityVerifier.d.ts +8 -0
- package/dist/identity/customerIdentityVerifier.js +101 -0
- package/dist/identity/customerMagicLinkHandler.d.ts +38 -0
- package/dist/identity/customerMagicLinkHandler.js +115 -0
- package/dist/identity/customerMeHandler.d.ts +11 -0
- package/dist/identity/customerMeHandler.js +46 -0
- package/dist/identity/customerPorts.d.ts +4 -0
- package/dist/identity/customerPorts.js +1 -0
- package/dist/identity/customerSessionBinding.d.ts +38 -0
- package/dist/identity/customerSessionBinding.js +80 -0
- package/dist/identity/customerSessionIssuer.d.ts +10 -0
- package/dist/identity/customerSessionIssuer.js +57 -0
- package/dist/identity/customerVerifyOtpHandler.d.ts +8 -0
- package/dist/identity/customerVerifyOtpHandler.js +41 -0
- package/dist/identity/directOperatorInvitationStore.d.ts +19 -0
- package/dist/identity/directOperatorInvitationStore.js +193 -0
- package/dist/identity/emailTrustPolicy.d.ts +14 -0
- package/dist/identity/emailTrustPolicy.js +16 -0
- package/dist/identity/identityPersonalDataContributor.d.ts +7 -0
- package/dist/identity/identityPersonalDataContributor.js +46 -0
- package/dist/identity/identityReadiness.d.ts +10 -0
- package/dist/identity/identityReadiness.js +25 -0
- package/dist/identity/index.d.ts +67 -0
- package/dist/identity/index.js +66 -0
- package/dist/identity/inviteAdminUserUseCase.d.ts +89 -0
- package/dist/identity/inviteAdminUserUseCase.js +75 -0
- package/dist/identity/jwtLibrary.d.ts +54 -0
- package/dist/identity/jwtLibrary.js +1 -0
- package/dist/identity/machineActorWriteFence.d.ts +4 -0
- package/dist/identity/machineActorWriteFence.js +13 -0
- package/dist/identity/magicLinkPorts.d.ts +28 -0
- package/dist/identity/magicLinkPorts.js +1 -0
- package/dist/identity/managedAccountLinkStore.d.ts +2 -0
- package/dist/identity/managedAccountLinkStore.js +93 -0
- package/dist/identity/managedAdminAuthVerifier.d.ts +7 -0
- package/dist/identity/managedAdminAuthVerifier.js +27 -0
- package/dist/identity/managedAdminUserGateways.d.ts +123 -0
- package/dist/identity/managedAdminUserGateways.js +127 -0
- package/dist/identity/managedClient.d.ts +90 -0
- package/dist/identity/managedClient.js +10 -0
- package/dist/identity/managedClientAuthLink.d.ts +3 -0
- package/dist/identity/managedClientAuthLink.js +115 -0
- package/dist/identity/managedCustomerMe.d.ts +13 -0
- package/dist/identity/managedCustomerMe.js +30 -0
- package/dist/identity/managedEmailTrustPolicy.d.ts +11 -0
- package/dist/identity/managedEmailTrustPolicy.js +105 -0
- package/dist/identity/managedIdentityVerifier.d.ts +2 -0
- package/dist/identity/managedIdentityVerifier.js +16 -0
- package/dist/identity/managedPrivacy.d.ts +4 -0
- package/dist/identity/managedPrivacy.js +39 -0
- package/dist/identity/messages.d.ts +29 -0
- package/dist/identity/messages.js +39 -0
- package/dist/identity/oidcIdentityVerifier.d.ts +45 -0
- package/dist/identity/oidcIdentityVerifier.js +309 -0
- package/dist/identity/oidcPrincipal.d.ts +2 -0
- package/dist/identity/oidcPrincipal.js +10 -0
- package/dist/identity/operatorAdminAuth.d.ts +6 -0
- package/dist/identity/operatorAdminAuth.js +13 -0
- package/dist/identity/operatorAuthHandlers.d.ts +18 -0
- package/dist/identity/operatorAuthHandlers.js +71 -0
- package/dist/identity/operatorIdentityVerifier.d.ts +8 -0
- package/dist/identity/operatorIdentityVerifier.js +19 -0
- package/dist/identity/operatorInvitationLifecycle.d.ts +7 -0
- package/dist/identity/operatorInvitationLifecycle.js +15 -0
- package/dist/identity/operatorInvitationPorts.d.ts +31 -0
- package/dist/identity/operatorInvitationPorts.js +1 -0
- package/dist/identity/operatorPorts.d.ts +37 -0
- package/dist/identity/operatorPorts.js +1 -0
- package/dist/identity/operatorSession.d.ts +13 -0
- package/dist/identity/operatorSession.js +31 -0
- package/dist/identity/operatorTokenAdminAuth.d.ts +15 -0
- package/dist/identity/operatorTokenAdminAuth.js +50 -0
- package/dist/identity/personalDataRegistry.d.ts +4 -0
- package/dist/identity/personalDataRegistry.js +11 -0
- package/dist/identity/platformAdminAuth.d.ts +15 -0
- package/dist/identity/platformAdminAuth.js +1 -0
- package/dist/identity/ports.d.ts +127 -0
- package/dist/identity/ports.js +38 -0
- package/dist/identity/postgresAccountLinkStore.d.ts +7 -0
- package/dist/identity/postgresAccountLinkStore.js +52 -0
- package/dist/identity/postgresCustomerMe.d.ts +3 -0
- package/dist/identity/postgresCustomerMe.js +21 -0
- package/dist/identity/postgresCustomerRecovery.d.ts +11 -0
- package/dist/identity/postgresCustomerRecovery.js +57 -0
- package/dist/identity/postgresOperatorIdentity.d.ts +5 -0
- package/dist/identity/postgresOperatorIdentity.js +137 -0
- package/dist/identity/postgresPrivacy.d.ts +7 -0
- package/dist/identity/postgresPrivacy.js +126 -0
- package/dist/identity/privacyHandlers.d.ts +10 -0
- package/dist/identity/privacyHandlers.js +35 -0
- package/dist/identity/privacyPorts.d.ts +72 -0
- package/dist/identity/privacyPorts.js +8 -0
- package/dist/identity/privacyUseCases.d.ts +5 -0
- package/dist/identity/privacyUseCases.js +55 -0
- package/dist/identity/reconcileAccountForPrincipal.d.ts +20 -0
- package/dist/identity/reconcileAccountForPrincipal.js +66 -0
- package/dist/identity/reconcileAccountHandler.d.ts +11 -0
- package/dist/identity/reconcileAccountHandler.js +30 -0
- package/dist/identity/removeAdminUserUseCase.d.ts +31 -0
- package/dist/identity/removeAdminUserUseCase.js +60 -0
- package/dist/identity/response.d.ts +3 -0
- package/dist/identity/response.js +2 -0
- package/dist/identity/sql.d.ts +44 -0
- package/dist/identity/sql.js +10 -0
- package/package.json +12 -2
- package/release-gates.json +22 -0
- package/sql/20260810180001_self_host_actor_auth.sql +40 -0
- package/sql/20260810204057_customer_identity_read.sql +31 -0
- package/sql/20260814090000_customer_selfservice_recovery.sql +477 -0
- package/sql/20260901143000_admin_membership_authority_foundation.sql +234 -0
- package/sql/20261018120000_identity_operator_invitations.sql +42 -0
- package/sql/20261018180000_identity_operator_challenges.sql +56 -0
- package/sql/20261018190000_identity_privacy_erasures.sql +15 -0
- package/src/identity/adminAuthBinding.ts +76 -0
- package/src/identity/adminAuthorization.ts +106 -0
- package/src/identity/adminContracts.ts +52 -0
- package/src/identity/adminMagicLinkHandler.ts +132 -0
- package/src/identity/adminMeHandler.ts +64 -0
- package/src/identity/adminPreflight.ts +46 -0
- package/src/identity/adminUserActionPorts.ts +78 -0
- package/src/identity/adminUserHandlers.ts +174 -0
- package/src/identity/authVerifyUrl.ts +56 -0
- package/src/identity/contracts/contracts.ts +15 -0
- package/src/identity/contracts/emailTrust.ts +5 -0
- package/src/identity/contracts/index.ts +8 -0
- package/src/identity/contracts/oidc.ts +10 -0
- package/src/identity/contracts/operatorAuth.ts +61 -0
- package/src/identity/contracts/operatorInvitations.ts +32 -0
- package/src/identity/contracts/ports.ts +25 -0
- package/src/identity/contracts/privacy.ts +86 -0
- package/src/identity/contracts/providerList.ts +13 -0
- package/src/identity/contracts/types.ts +25 -0
- package/src/identity/contribution.ts +29 -0
- package/src/identity/customerAuth.ts +3 -0
- package/src/identity/customerAuthorization.ts +38 -0
- package/src/identity/customerContracts.ts +52 -0
- package/src/identity/customerIdentityBinding.ts +125 -0
- package/src/identity/customerIdentityVerifier.ts +141 -0
- package/src/identity/customerMagicLinkHandler.ts +171 -0
- package/src/identity/customerMeHandler.ts +64 -0
- package/src/identity/customerPorts.ts +2 -0
- package/src/identity/customerSessionBinding.ts +114 -0
- package/src/identity/customerSessionIssuer.ts +71 -0
- package/src/identity/customerVerifyOtpHandler.ts +44 -0
- package/src/identity/directOperatorInvitationStore.ts +178 -0
- package/src/identity/emailTrustPolicy.ts +16 -0
- package/src/identity/identityPersonalDataContributor.ts +48 -0
- package/src/identity/identityReadiness.ts +26 -0
- package/src/identity/index.ts +69 -0
- package/src/identity/inviteAdminUserUseCase.ts +134 -0
- package/src/identity/jwtLibrary.ts +24 -0
- package/src/identity/machineActorWriteFence.ts +19 -0
- package/src/identity/magicLinkPorts.ts +7 -0
- package/src/identity/managedAccountLinkStore.ts +105 -0
- package/src/identity/managedAdminAuthVerifier.ts +40 -0
- package/src/identity/managedAdminUserGateways.ts +164 -0
- package/src/identity/managedClient.ts +58 -0
- package/src/identity/managedClientAuthLink.ts +120 -0
- package/src/identity/managedCustomerMe.ts +52 -0
- package/src/identity/managedEmailTrustPolicy.ts +90 -0
- package/src/identity/managedIdentityVerifier.ts +20 -0
- package/src/identity/managedPrivacy.ts +37 -0
- package/src/identity/messages.ts +41 -0
- package/src/identity/oidcIdentityVerifier.ts +229 -0
- package/src/identity/oidcPrincipal.ts +10 -0
- package/src/identity/operatorAdminAuth.ts +14 -0
- package/src/identity/operatorAuthHandlers.ts +52 -0
- package/src/identity/operatorIdentityVerifier.ts +19 -0
- package/src/identity/operatorInvitationLifecycle.ts +14 -0
- package/src/identity/operatorInvitationPorts.ts +11 -0
- package/src/identity/operatorPorts.ts +11 -0
- package/src/identity/operatorSession.ts +30 -0
- package/src/identity/operatorTokenAdminAuth.ts +79 -0
- package/src/identity/personalDataRegistry.ts +8 -0
- package/src/identity/platformAdminAuth.ts +7 -0
- package/src/identity/ports.ts +158 -0
- package/src/identity/postgresAccountLinkStore.ts +49 -0
- package/src/identity/postgresCustomerMe.ts +33 -0
- package/src/identity/postgresCustomerRecovery.ts +72 -0
- package/src/identity/postgresOperatorIdentity.ts +123 -0
- package/src/identity/postgresPrivacy.ts +117 -0
- package/src/identity/privacyHandlers.ts +33 -0
- package/src/identity/privacyPorts.ts +52 -0
- package/src/identity/privacyUseCases.ts +48 -0
- package/src/identity/reconcileAccountForPrincipal.ts +99 -0
- package/src/identity/reconcileAccountHandler.ts +36 -0
- package/src/identity/removeAdminUserUseCase.ts +85 -0
- package/src/identity/response.ts +3 -0
- package/src/identity/sql.ts +10 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { IdentityJwtLibraryLoader } from "./jwtLibrary.js";
|
|
2
|
+
import { authenticateCustomerUser } from "./customerAuthorization.js";
|
|
3
|
+
import { readBearerToken } from "./adminAuthorization.js";
|
|
4
|
+
import { createPostgresCustomerMePort } from "./postgresCustomerMe.js";
|
|
5
|
+
import { createSupabaseCustomerMePort as createManagedCustomerMePort } from "./managedCustomerMe.js";
|
|
6
|
+
import { createCustomerIdentityVerifier } from "./customerIdentityVerifier.js";
|
|
7
|
+
import type { CustomerMeDeps } from "./customerMeHandler.js";
|
|
8
|
+
import { resolveBundleId } from "../kernel/index.js";
|
|
9
|
+
type Env = Record<string, string | undefined>;
|
|
10
|
+
type CustomerRequest = Parameters<typeof readBearerToken>[0];
|
|
11
|
+
type ManagedReadEnv = {
|
|
12
|
+
url: string;
|
|
13
|
+
anonKey: string;
|
|
14
|
+
};
|
|
15
|
+
type CreateCustomerClient = (env: ManagedReadEnv, token: string | null) => unknown;
|
|
16
|
+
type ActorGateway = {
|
|
17
|
+
asActor<T>(claims: {
|
|
18
|
+
role: "authenticated";
|
|
19
|
+
sub: string;
|
|
20
|
+
}, work: (gateway: unknown) => Promise<T>): Promise<T>;
|
|
21
|
+
};
|
|
22
|
+
export type CustomerIdentityBindingOptions = {
|
|
23
|
+
authenticateManaged?: typeof authenticateCustomerUser;
|
|
24
|
+
createManagedClient?: CreateCustomerClient;
|
|
25
|
+
createManagedPort?: typeof createManagedCustomerMePort;
|
|
26
|
+
readManagedEnv?: () => ManagedReadEnv | null;
|
|
27
|
+
readToken?: typeof readBearerToken;
|
|
28
|
+
createPostgresGateway?: (env: Env) => ActorGateway;
|
|
29
|
+
createPostgresPort?: typeof createPostgresCustomerMePort;
|
|
30
|
+
createVerifier?: (config: Parameters<typeof createCustomerIdentityVerifier>[0]) => ReturnType<typeof createCustomerIdentityVerifier>;
|
|
31
|
+
resolveBundle?: typeof resolveBundleId;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Select the customer identity/read composition for one request. The direct
|
|
35
|
+
* path constructs its profile port only inside one actor-scoped transaction;
|
|
36
|
+
* it never uses a generic service lane or caller-provided token role.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCustomerIdentityBindingFactory(defaults: {
|
|
39
|
+
loadJwt: IdentityJwtLibraryLoader;
|
|
40
|
+
createManagedClient: CreateCustomerClient;
|
|
41
|
+
readManagedEnv: (env: Env) => ManagedReadEnv | null;
|
|
42
|
+
bindRequestActorDataPort: (token: string, env: Env) => ActorGateway | null;
|
|
43
|
+
}): (req: CustomerRequest, env: Env, options?: CustomerIdentityBindingOptions) => Omit<CustomerMeDeps, "responseSchema"> | null;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { authenticateCustomerUser, authenticateCustomerUserWith, } from "./customerAuthorization.js";
|
|
2
|
+
import { readBearerToken, } from "./adminAuthorization.js";
|
|
3
|
+
import { createPostgresCustomerMePort } from "./postgresCustomerMe.js";
|
|
4
|
+
import { createSupabaseCustomerMePort as createManagedCustomerMePort } from "./managedCustomerMe.js";
|
|
5
|
+
import { createCustomerIdentityVerifier } from "./customerIdentityVerifier.js";
|
|
6
|
+
import { resolveBundleId } from "../kernel/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Select the customer identity/read composition for one request. The direct
|
|
9
|
+
* path constructs its profile port only inside one actor-scoped transaction;
|
|
10
|
+
* it never uses a generic service lane or caller-provided token role.
|
|
11
|
+
*/
|
|
12
|
+
export function createCustomerIdentityBindingFactory(defaults) {
|
|
13
|
+
return function createCustomerIdentityBinding(req, env, options = {}) {
|
|
14
|
+
const accessToken = (options.readToken ?? readBearerToken)(req);
|
|
15
|
+
const bundleId = (options.resolveBundle ?? resolveBundleId)(env);
|
|
16
|
+
if (bundleId !== "node-postgres") {
|
|
17
|
+
const readEnv = options.readManagedEnv ?? (() => defaults.readManagedEnv(env));
|
|
18
|
+
const managedEnv = readEnv();
|
|
19
|
+
if (!managedEnv)
|
|
20
|
+
return null;
|
|
21
|
+
const client = (options.createManagedClient ?? defaults.createManagedClient)(managedEnv, accessToken);
|
|
22
|
+
return {
|
|
23
|
+
authenticateUser: () => (options.authenticateManaged ?? authenticateCustomerUser)(client, accessToken),
|
|
24
|
+
mePort: (options.createManagedPort ?? createManagedCustomerMePort)(client),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const gateway = (options.createPostgresGateway ?? createActorGateway)(env);
|
|
28
|
+
const createPort = options.createPostgresPort ?? createPostgresCustomerMePort;
|
|
29
|
+
const createVerifier = options.createVerifier ?? ((config) => createCustomerIdentityVerifier(config, defaults.loadJwt));
|
|
30
|
+
const verifierConfig = {
|
|
31
|
+
audience: env.CUSTOMER_IDENTITY_AUDIENCE ?? "",
|
|
32
|
+
issuer: env.CUSTOMER_IDENTITY_ISSUER ?? "",
|
|
33
|
+
jwks: env.CUSTOMER_IDENTITY_JWKS ?? "",
|
|
34
|
+
};
|
|
35
|
+
let verifiedPrincipalId = null;
|
|
36
|
+
const mePort = {
|
|
37
|
+
getCustomerMe: (principalId) => {
|
|
38
|
+
if (principalId !== verifiedPrincipalId) {
|
|
39
|
+
return Promise.reject(new Error("customer_principal_mismatch"));
|
|
40
|
+
}
|
|
41
|
+
return gateway.asActor({ role: "authenticated", sub: verifiedPrincipalId }, (actorGateway) => createPort(actorGateway).getCustomerMe(principalId));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
async authenticateUser() {
|
|
46
|
+
const authentication = await authenticateCustomerUserWith(createVerifier(verifierConfig), accessToken);
|
|
47
|
+
if (authentication.ok)
|
|
48
|
+
verifiedPrincipalId = authentication.userId;
|
|
49
|
+
return authentication;
|
|
50
|
+
},
|
|
51
|
+
mePort,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
function createActorGateway(env) {
|
|
55
|
+
const gateway = defaults.bindRequestActorDataPort("", env);
|
|
56
|
+
if (!gateway)
|
|
57
|
+
throw new Error("customer identity data gateway is unavailable");
|
|
58
|
+
return gateway;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IdentityJwtLibraryLoader } from "./jwtLibrary.js";
|
|
2
|
+
import type { IdentityVerifierPort } from "./ports.js";
|
|
3
|
+
export interface CustomerIdentityVerifierConfig {
|
|
4
|
+
audience: string;
|
|
5
|
+
issuer: string;
|
|
6
|
+
jwks: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function createCustomerIdentityVerifier(config: CustomerIdentityVerifierConfig, loadJwt: IdentityJwtLibraryLoader): IdentityVerifierPort;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const MAX_JWKS_BYTES = 12_288;
|
|
2
|
+
const MAX_TOKEN_BYTES = 8_192;
|
|
3
|
+
const MAX_CLAIM_BYTES = 256;
|
|
4
|
+
const KID = /^[A-Za-z0-9._-]{1,64}$/;
|
|
5
|
+
const BASE64URL_COORDINATE = /^[A-Za-z0-9_-]{43}$/;
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
7
|
+
const JWK_FIELDS = new Set(["alg", "crv", "kid", "kty", "use", "x", "y"]);
|
|
8
|
+
export function createCustomerIdentityVerifier(config, loadJwt) {
|
|
9
|
+
const issuer = readClaim(config.issuer, "issuer");
|
|
10
|
+
const audience = readClaim(config.audience, "audience");
|
|
11
|
+
const jwks = readJwks(config.jwks);
|
|
12
|
+
let keys = null;
|
|
13
|
+
let library;
|
|
14
|
+
const getLibrary = () => library ??= loadJwt();
|
|
15
|
+
const importedKeys = () => keys ??= getLibrary().then(({ importJWK }) => Promise.all(jwks.map(async (jwk) => [jwk.kid, await importJWK(jwk, "ES256")]))).then((entries) => new Map(entries));
|
|
16
|
+
return {
|
|
17
|
+
async verifyAccessToken(accessToken) {
|
|
18
|
+
if (typeof accessToken !== "string" || accessToken.length === 0 || accessToken.length > MAX_TOKEN_BYTES) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const resolvedKeys = await importedKeys();
|
|
22
|
+
const { jwtVerify } = await getLibrary();
|
|
23
|
+
try {
|
|
24
|
+
const verified = await jwtVerify(accessToken, async (header) => {
|
|
25
|
+
if (header.alg !== "ES256" || typeof header.kid !== "string" || !KID.test(header.kid)) {
|
|
26
|
+
throw new Error("customer identity token header rejected");
|
|
27
|
+
}
|
|
28
|
+
const key = resolvedKeys.get(header.kid);
|
|
29
|
+
if (!key)
|
|
30
|
+
throw new Error("customer identity token key rejected");
|
|
31
|
+
return key;
|
|
32
|
+
}, { algorithms: ["ES256"], audience, issuer });
|
|
33
|
+
if (typeof verified.payload.sub !== "string" || !UUID.test(verified.payload.sub))
|
|
34
|
+
return null;
|
|
35
|
+
return { email: null, emailVerified: false, principalId: verified.payload.sub };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function readClaim(value, label) {
|
|
44
|
+
if (typeof value !== "string"
|
|
45
|
+
|| value.length === 0
|
|
46
|
+
|| value.length > MAX_CLAIM_BYTES
|
|
47
|
+
|| value.trim() !== value
|
|
48
|
+
|| [...value].some((character) => {
|
|
49
|
+
const code = character.charCodeAt(0);
|
|
50
|
+
return code <= 0x1f || code === 0x7f;
|
|
51
|
+
})) {
|
|
52
|
+
throw new Error(`customer identity ${label} configuration is invalid`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function readJwks(value) {
|
|
57
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_JWKS_BYTES) {
|
|
58
|
+
throw new Error("customer identity JWKS configuration is invalid");
|
|
59
|
+
}
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(value);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new Error("customer identity JWKS configuration is invalid");
|
|
66
|
+
}
|
|
67
|
+
if (!isRecord(parsed) || !hasOnlyFields(parsed, new Set(["keys"])) || !Array.isArray(parsed.keys)) {
|
|
68
|
+
throw new Error("customer identity JWKS configuration is invalid");
|
|
69
|
+
}
|
|
70
|
+
if (parsed.keys.length < 1 || parsed.keys.length > 2) {
|
|
71
|
+
throw new Error("customer identity JWKS configuration is invalid");
|
|
72
|
+
}
|
|
73
|
+
const keys = parsed.keys.map(readJwk);
|
|
74
|
+
if (new Set(keys.map((key) => key.kid)).size !== keys.length) {
|
|
75
|
+
throw new Error("customer identity JWKS configuration is invalid");
|
|
76
|
+
}
|
|
77
|
+
return keys;
|
|
78
|
+
}
|
|
79
|
+
function readJwk(value) {
|
|
80
|
+
if (!isRecord(value)
|
|
81
|
+
|| !hasOnlyFields(value, JWK_FIELDS)
|
|
82
|
+
|| value.kty !== "EC"
|
|
83
|
+
|| value.crv !== "P-256"
|
|
84
|
+
|| value.alg !== "ES256"
|
|
85
|
+
|| value.use !== "sig"
|
|
86
|
+
|| typeof value.kid !== "string"
|
|
87
|
+
|| !KID.test(value.kid)
|
|
88
|
+
|| typeof value.x !== "string"
|
|
89
|
+
|| !BASE64URL_COORDINATE.test(value.x)
|
|
90
|
+
|| typeof value.y !== "string"
|
|
91
|
+
|| !BASE64URL_COORDINATE.test(value.y)) {
|
|
92
|
+
throw new Error("customer identity JWKS configuration is invalid");
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function isRecord(value) {
|
|
97
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
function hasOnlyFields(value, fields) {
|
|
100
|
+
return Object.keys(value).every((field) => fields.has(field));
|
|
101
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type IdentityResponseOptions } from "./response.js";
|
|
2
|
+
import type { HttpRequest, HttpResponse } from "../http/index.js";
|
|
3
|
+
import type { MagicLinkAuthClient, MagicLinkRateLimitClient, MagicLinkDecision } from "./magicLinkPorts.js";
|
|
4
|
+
import { resolveCustomerSessionBinding } from "./customerSessionBinding.js";
|
|
5
|
+
import type { z } from "zod";
|
|
6
|
+
export type CustomerMagicLinkEnvironment = {
|
|
7
|
+
url: string;
|
|
8
|
+
anonKey: string;
|
|
9
|
+
serviceRoleKey: string;
|
|
10
|
+
};
|
|
11
|
+
export interface CustomerMagicLinkHandlerOptions<E extends CustomerMagicLinkEnvironment = CustomerMagicLinkEnvironment> extends IdentityResponseOptions {
|
|
12
|
+
enabled: () => boolean;
|
|
13
|
+
env: Record<string, string | undefined>;
|
|
14
|
+
disabledDetails?: Record<string, unknown>;
|
|
15
|
+
requestSchema: z.ZodType<{
|
|
16
|
+
email: string;
|
|
17
|
+
locale: "pl" | "en";
|
|
18
|
+
returnTo?: string;
|
|
19
|
+
}>;
|
|
20
|
+
responseSchema: z.ZodType<{
|
|
21
|
+
accepted: true;
|
|
22
|
+
}>;
|
|
23
|
+
readEnvironment: () => E | null;
|
|
24
|
+
createCustomerClient: (env: E, token: null) => MagicLinkAuthClient;
|
|
25
|
+
createCustomerServiceClient: (env: E) => unknown;
|
|
26
|
+
checkAttempt: (input: {
|
|
27
|
+
client: MagicLinkRateLimitClient;
|
|
28
|
+
ip: string;
|
|
29
|
+
email: string;
|
|
30
|
+
}) => Promise<MagicLinkDecision>;
|
|
31
|
+
extractClientIp: (headers: HttpRequest["headers"]) => string;
|
|
32
|
+
padResponseTime: (startedAt: number, floor: number) => Promise<void>;
|
|
33
|
+
redirectOrigins?: readonly string[];
|
|
34
|
+
resolveOrigin?: (req: HttpRequest) => string | null;
|
|
35
|
+
callbackPath: (locale: "pl" | "en") => string;
|
|
36
|
+
resolveSession: typeof resolveCustomerSessionBinding;
|
|
37
|
+
}
|
|
38
|
+
export declare function createCustomerMagicLinkHandler<E extends CustomerMagicLinkEnvironment>(options: CustomerMagicLinkHandlerOptions<E>): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createMagicLinkOriginResolver, normalizeMagicLinkReturnPath } from "./authVerifyUrl.js";
|
|
2
|
+
import { createIdentityResponder } from "./response.js";
|
|
3
|
+
import { resolveBundleId } from "../kernel/index.js";
|
|
4
|
+
const MAGIC_LINK_RESPONSE_FLOOR_MS = 400;
|
|
5
|
+
export function createCustomerMagicLinkHandler(options) {
|
|
6
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder(options);
|
|
7
|
+
const readRequestOrigin = options.resolveOrigin ?? createMagicLinkOriginResolver(options.redirectOrigins ?? []);
|
|
8
|
+
const { enabled: customerAuthUiEnabled, env: environment, readEnvironment: readCustomerSelfServiceEnv, createCustomerClient, createCustomerServiceClient, checkAttempt: checkAndRecordCustomerMagicLinkAttempt, extractClientIp, padResponseTime, requestSchema: customerMagicLinkRequestSchema, responseSchema: customerMagicLinkResponseSchema, resolveSession, callbackPath: customerAuthCallbackPath } = options;
|
|
9
|
+
return async (req, res) => {
|
|
10
|
+
if (!customerAuthUiEnabled()) {
|
|
11
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Customer accounts are disabled", {
|
|
12
|
+
details: {
|
|
13
|
+
feature: "customer_auth",
|
|
14
|
+
...options.disabledDetails,
|
|
15
|
+
reason: "feature_flag_disabled",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (req.method !== "POST") {
|
|
21
|
+
sendMethodNotAllowed(res, ["POST"]);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const request = customerMagicLinkRequestSchema.safeParse(req.body);
|
|
25
|
+
if (!request.success) {
|
|
26
|
+
sendBffError(res, "BAD_REQUEST", "Invalid magic-link request", {
|
|
27
|
+
details: request.error.flatten(),
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
const origin = readRequestOrigin(req);
|
|
33
|
+
if (!origin) {
|
|
34
|
+
await padResponseTime(startedAt, MAGIC_LINK_RESPONSE_FLOOR_MS);
|
|
35
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Magic link origin is not configured", {
|
|
36
|
+
details: { reason: "magic_link_origin_not_configured" },
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (resolveBundleId(environment) === "node-postgres") {
|
|
41
|
+
const resolved = resolveSession(environment);
|
|
42
|
+
if (!resolved.binding) {
|
|
43
|
+
await padResponseTime(startedAt, MAGIC_LINK_RESPONSE_FLOOR_MS);
|
|
44
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Customer auth is not configured", {
|
|
45
|
+
details: { feature: "customer_auth", reason: resolved.error },
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await resolved.binding.requestChallenge(request.data.email);
|
|
50
|
+
await padResponseTime(startedAt, MAGIC_LINK_RESPONSE_FLOOR_MS);
|
|
51
|
+
sendAccepted(res);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const env = readCustomerSelfServiceEnv();
|
|
55
|
+
if (!env) {
|
|
56
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Customer auth is not configured", {
|
|
57
|
+
details: {
|
|
58
|
+
feature: "customer_auth",
|
|
59
|
+
requiredEnv: "SUPABASE_URL/SUPABASE_ANON_KEY/SUPABASE_SERVICE_ROLE_KEY",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const anonymousAuthClient = createCustomerClient(env, null);
|
|
65
|
+
const rateLimitClient = createCustomerServiceClient(env);
|
|
66
|
+
const decision = await checkAndRecordCustomerMagicLinkAttempt({
|
|
67
|
+
client: rateLimitClient,
|
|
68
|
+
ip: extractClientIp(req.headers),
|
|
69
|
+
email: request.data.email,
|
|
70
|
+
});
|
|
71
|
+
if (!decision.allowed && decision.reason === "rpc_error") {
|
|
72
|
+
console.warn("customer_magic_link_rate_limit_unavailable", JSON.stringify({ reason: decision.reason }));
|
|
73
|
+
await padResponseTime(startedAt, MAGIC_LINK_RESPONSE_FLOOR_MS);
|
|
74
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Magic link service temporarily unavailable");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (decision.allowed) {
|
|
78
|
+
const outcome = await sendMagicLink(anonymousAuthClient, request.data.email, origin, request.data.locale, normalizeMagicLinkReturnPath(request.data.returnTo, origin));
|
|
79
|
+
console.info("customer_magic_link_outcome", JSON.stringify({ outcome: outcome.status, code: outcome.code ?? null, locale: request.data.locale }));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.warn("customer_magic_link_outcome", JSON.stringify({ outcome: "rate_limited", reason: decision.reason ?? "unknown" }));
|
|
83
|
+
}
|
|
84
|
+
await padResponseTime(startedAt, MAGIC_LINK_RESPONSE_FLOOR_MS);
|
|
85
|
+
sendAccepted(res);
|
|
86
|
+
};
|
|
87
|
+
async function sendMagicLink(client, email, origin, locale, returnTo) {
|
|
88
|
+
try {
|
|
89
|
+
const callbackUrl = new URL(customerAuthCallbackPath(locale), origin);
|
|
90
|
+
if (returnTo)
|
|
91
|
+
callbackUrl.searchParams.set("returnTo", returnTo);
|
|
92
|
+
const { error } = await client.auth.signInWithOtp({
|
|
93
|
+
email,
|
|
94
|
+
options: {
|
|
95
|
+
// First login confirms the already provisioned principal through mailbox proof.
|
|
96
|
+
shouldCreateUser: false,
|
|
97
|
+
emailRedirectTo: callbackUrl.toString(),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (error) {
|
|
101
|
+
console.warn("customer_magic_link_send_failed", JSON.stringify({ code: error.name }));
|
|
102
|
+
return { status: "auth_error", code: error.name };
|
|
103
|
+
}
|
|
104
|
+
return { status: "sent" };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const code = error instanceof Error ? error.name : "unknown";
|
|
108
|
+
console.warn("customer_magic_link_send_threw", JSON.stringify({ error: code }));
|
|
109
|
+
return { status: "threw", code };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function sendAccepted(res) {
|
|
113
|
+
sendBffSuccess(res, customerMagicLinkResponseSchema.parse({ accepted: true }));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type IdentityResponseOptions } from "./response.js";
|
|
2
|
+
import type { HttpRequest, HttpResponse } from "../http/index.js";
|
|
3
|
+
import { customerMeResponseSchema } from "./customerContracts.js";
|
|
4
|
+
import type { CustomerMePort } from "./customerPorts.js";
|
|
5
|
+
import type { CustomerUserAuthenticationResult } from "./customerAuth.js";
|
|
6
|
+
export interface CustomerMeDeps extends IdentityResponseOptions {
|
|
7
|
+
responseSchema: typeof customerMeResponseSchema;
|
|
8
|
+
mePort: CustomerMePort;
|
|
9
|
+
authenticateUser: (req: HttpRequest) => Promise<CustomerUserAuthenticationResult>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createCustomerMeHandler({ mePort, authenticateUser, responseSchema, responseRequestId }: CustomerMeDeps): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createIdentityResponder } from "./response.js";
|
|
2
|
+
import { customerMeRequestSchema, } from "./customerContracts.js";
|
|
3
|
+
export function createCustomerMeHandler({ mePort, authenticateUser, responseSchema, responseRequestId }) {
|
|
4
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = createIdentityResponder({ responseRequestId });
|
|
5
|
+
return async function handler(req, res) {
|
|
6
|
+
if (req.method !== "GET") {
|
|
7
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
let authentication;
|
|
11
|
+
try {
|
|
12
|
+
authentication = await authenticateUser(req);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Customer authentication failed");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (authentication.ok === false) {
|
|
19
|
+
sendBffError(res, authentication.code, authentication.message);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const request = customerMeRequestSchema.safeParse(req.query ?? {});
|
|
23
|
+
if (!request.success) {
|
|
24
|
+
sendBffError(res, "BAD_REQUEST", "Invalid customer me request", {
|
|
25
|
+
details: request.error.flatten(),
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const result = await mePort.getCustomerMe(authentication.userId);
|
|
31
|
+
if (!result) {
|
|
32
|
+
sendBffError(res, "FORBIDDEN", "No customer account linked to this session");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const response = responseSchema.safeParse(result);
|
|
36
|
+
if (!response.success) {
|
|
37
|
+
sendBffError(res, "INVALID_RESPONSE", "Customer me returned invalid response");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
sendBffSuccess(res, response.data);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Customer me read failed");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { IdentityJwtLibraryLoader } from "./jwtLibrary.js";
|
|
2
|
+
import type { CustomerChallengeDeliveryPort, CustomerChallengeStore, CustomerSessionIssuerPort } from "./ports.js";
|
|
3
|
+
type Env = Record<string, string | undefined>;
|
|
4
|
+
export type CapturedCustomerChallenge = {
|
|
5
|
+
readonly email: string;
|
|
6
|
+
readonly token: string;
|
|
7
|
+
readonly expiresAt: string;
|
|
8
|
+
};
|
|
9
|
+
/** Deterministic no-egress reference delivery; deployments inject mail. */
|
|
10
|
+
export declare function createCapturedCustomerChallengeDelivery(sink?: (challenge: CapturedCustomerChallenge) => Promise<void>): CustomerChallengeDeliveryPort;
|
|
11
|
+
export interface CustomerSessionBinding {
|
|
12
|
+
requestChallenge(email: string): Promise<void>;
|
|
13
|
+
verifyChallenge(token: string): Promise<{
|
|
14
|
+
accessToken: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
user: {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
};
|
|
20
|
+
} | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare function resolveCustomerSessionBinding(env: Env, options?: {
|
|
23
|
+
createStore?: (connectionString: string) => CustomerChallengeStore & {
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
delivery?: CustomerChallengeDeliveryPort;
|
|
27
|
+
issuer?: CustomerSessionIssuerPort;
|
|
28
|
+
loadJwt?: IdentityJwtLibraryLoader;
|
|
29
|
+
generateToken?: () => string;
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
}): {
|
|
32
|
+
binding: CustomerSessionBinding;
|
|
33
|
+
error?: undefined;
|
|
34
|
+
} | {
|
|
35
|
+
binding?: undefined;
|
|
36
|
+
error: string;
|
|
37
|
+
};
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createCustomerSessionIssuer } from "./customerSessionIssuer.js";
|
|
3
|
+
import { createPostgresCustomerChallengeStore } from "./postgresCustomerRecovery.js";
|
|
4
|
+
import { resolveBundleId } from "../kernel/index.js";
|
|
5
|
+
/** Deterministic no-egress reference delivery; deployments inject mail. */
|
|
6
|
+
export function createCapturedCustomerChallengeDelivery(sink = async () => { }) {
|
|
7
|
+
return { deliver: sink };
|
|
8
|
+
}
|
|
9
|
+
export function resolveCustomerSessionBinding(env, options = {}) {
|
|
10
|
+
if (resolveBundleId(env) !== "node-postgres")
|
|
11
|
+
return { error: "customer_session_external_bundle" };
|
|
12
|
+
const connectionString = env.DATABASE_URL?.trim() ?? "";
|
|
13
|
+
if (!connectionString)
|
|
14
|
+
return { error: "database_url_required" };
|
|
15
|
+
let issuer;
|
|
16
|
+
try {
|
|
17
|
+
if (!options.issuer && !options.loadJwt)
|
|
18
|
+
throw new Error("identity_jwt_library_required");
|
|
19
|
+
issuer = options.issuer ?? createCustomerSessionIssuer({
|
|
20
|
+
audience: env.CUSTOMER_IDENTITY_AUDIENCE ?? "",
|
|
21
|
+
issuer: env.CUSTOMER_IDENTITY_ISSUER ?? "",
|
|
22
|
+
keyId: env.CUSTOMER_IDENTITY_KEY_ID ?? "",
|
|
23
|
+
privateJwk: env.CUSTOMER_IDENTITY_PRIVATE_JWK ?? "",
|
|
24
|
+
ttlSeconds: readTtl(env.CUSTOMER_IDENTITY_SESSION_TTL_SECONDS, 3_600),
|
|
25
|
+
}, options.loadJwt);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { error: "customer_session_issuer_invalid" };
|
|
29
|
+
}
|
|
30
|
+
const createStore = options.createStore ?? ((url) => createPostgresCustomerChallengeStore({ connectionString: url }));
|
|
31
|
+
const delivery = options.delivery ?? createCapturedCustomerChallengeDelivery();
|
|
32
|
+
const generateToken = options.generateToken ?? (() => randomBytes(32).toString("base64url"));
|
|
33
|
+
const now = options.now ?? (() => new Date());
|
|
34
|
+
const challengeTtlSeconds = readTtl(env.CUSTOMER_IDENTITY_CHALLENGE_TTL_SECONDS, 600);
|
|
35
|
+
return { binding: {
|
|
36
|
+
async requestChallenge(email) {
|
|
37
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
38
|
+
const token = generateToken();
|
|
39
|
+
const requestedAt = now();
|
|
40
|
+
const expiresAt = new Date(requestedAt.getTime() + challengeTtlSeconds * 1000).toISOString();
|
|
41
|
+
const store = createStore(connectionString);
|
|
42
|
+
try {
|
|
43
|
+
const result = await store.issue({ email: normalizedEmail, tokenHash: hash(token), expiresAt });
|
|
44
|
+
if (result.deliverable)
|
|
45
|
+
await delivery.deliver({ email: normalizedEmail, token, expiresAt });
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await store.close();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async verifyChallenge(token) {
|
|
52
|
+
const store = createStore(connectionString);
|
|
53
|
+
try {
|
|
54
|
+
const redeemed = await store.redeem({ tokenHash: hash(token), now: now().toISOString() });
|
|
55
|
+
if (!redeemed)
|
|
56
|
+
return null;
|
|
57
|
+
const session = await issuer.issue(redeemed);
|
|
58
|
+
return {
|
|
59
|
+
...session,
|
|
60
|
+
user: { id: redeemed.principalId, email: redeemed.email },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await store.close();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
} };
|
|
68
|
+
}
|
|
69
|
+
function hash(value) {
|
|
70
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
71
|
+
}
|
|
72
|
+
function readTtl(value, fallback) {
|
|
73
|
+
if (!value)
|
|
74
|
+
return fallback;
|
|
75
|
+
const parsed = Number(value);
|
|
76
|
+
if (!Number.isInteger(parsed) || parsed < 300 || parsed > 86_400) {
|
|
77
|
+
throw new Error("customer identity ttl invalid");
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IdentityJwtLibraryLoader } from "./jwtLibrary.js";
|
|
2
|
+
import type { CustomerSessionIssuerPort } from "./ports.js";
|
|
3
|
+
export interface CustomerSessionIssuerConfig {
|
|
4
|
+
audience: string;
|
|
5
|
+
issuer: string;
|
|
6
|
+
keyId: string;
|
|
7
|
+
privateJwk: string;
|
|
8
|
+
ttlSeconds: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function createCustomerSessionIssuer(config: CustomerSessionIssuerConfig, loadJwt: IdentityJwtLibraryLoader): CustomerSessionIssuerPort;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
2
|
+
const KID = /^[A-Za-z0-9._-]{1,64}$/;
|
|
3
|
+
export function createCustomerSessionIssuer(config, loadJwt) {
|
|
4
|
+
const audience = claim(config.audience, "audience");
|
|
5
|
+
const issuer = claim(config.issuer, "issuer");
|
|
6
|
+
if (!KID.test(config.keyId))
|
|
7
|
+
throw new Error("customer session key id configuration is invalid");
|
|
8
|
+
if (!Number.isInteger(config.ttlSeconds) || config.ttlSeconds < 300 || config.ttlSeconds > 86_400) {
|
|
9
|
+
throw new Error("customer session ttl configuration is invalid");
|
|
10
|
+
}
|
|
11
|
+
const privateKey = readPrivateKey(config.privateJwk);
|
|
12
|
+
const library = loadJwt();
|
|
13
|
+
const key = library.then(({ importJWK }) => importJWK(privateKey, "ES256"));
|
|
14
|
+
return {
|
|
15
|
+
async issue({ principalId, email }) {
|
|
16
|
+
if (!UUID.test(principalId))
|
|
17
|
+
throw new Error("customer session principal is invalid");
|
|
18
|
+
const normalizedEmail = email.trim().toLowerCase();
|
|
19
|
+
if (!normalizedEmail || normalizedEmail.length > 320) {
|
|
20
|
+
throw new Error("customer session email is invalid");
|
|
21
|
+
}
|
|
22
|
+
const issuedAt = Math.floor(Date.now() / 1000);
|
|
23
|
+
const expiresAt = issuedAt + config.ttlSeconds;
|
|
24
|
+
const { SignJWT } = await library;
|
|
25
|
+
const accessToken = await new SignJWT({ email: normalizedEmail, email_verified: true })
|
|
26
|
+
.setProtectedHeader({ alg: "ES256", kid: config.keyId, typ: "JWT" })
|
|
27
|
+
.setSubject(principalId)
|
|
28
|
+
.setIssuer(issuer)
|
|
29
|
+
.setAudience(audience)
|
|
30
|
+
.setIssuedAt(issuedAt)
|
|
31
|
+
.setExpirationTime(expiresAt)
|
|
32
|
+
.sign(await key);
|
|
33
|
+
return { accessToken, expiresAt: new Date(expiresAt * 1000).toISOString() };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function readPrivateKey(value) {
|
|
38
|
+
if (!value || value.length > 16_384)
|
|
39
|
+
throw new Error("customer session private JWK configuration is invalid");
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(value);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error("customer session private JWK configuration is invalid");
|
|
46
|
+
}
|
|
47
|
+
if (parsed.kty !== "EC" || parsed.crv !== "P-256" || parsed.alg !== "ES256" || typeof parsed.d !== "string") {
|
|
48
|
+
throw new Error("customer session private JWK configuration is invalid");
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
function claim(value, label) {
|
|
53
|
+
if (!value || value.length > 256 || value.trim() !== value) {
|
|
54
|
+
throw new Error(`customer session ${label} configuration is invalid`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type IdentityResponseOptions } from "./response.js";
|
|
2
|
+
import type { HttpRequest, HttpResponse } from "../http/index.js";
|
|
3
|
+
import { resolveCustomerSessionBinding } from "./customerSessionBinding.js";
|
|
4
|
+
export declare function createCustomerVerifyOtpHandler(options: IdentityResponseOptions & {
|
|
5
|
+
enabled: () => boolean;
|
|
6
|
+
env: Record<string, string | undefined>;
|
|
7
|
+
resolveSession: typeof resolveCustomerSessionBinding;
|
|
8
|
+
}): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|