@zlooks.cn/password-auth-server 1.0.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.
Files changed (116) hide show
  1. package/.hile-rsc/build-mtie8wg2-ca38f847/client-browser/chunks/chunk-Q3WJINFG.js +48386 -0
  2. package/.hile-rsc/build-mtie8wg2-ca38f847/client-browser/src-plugin-page-frame-07c4de56a6-3F24AYCH.css +170 -0
  3. package/.hile-rsc/build-mtie8wg2-ca38f847/client-browser/src-plugin-page-frame-07c4de56a6-OCDODDFY.js +440 -0
  4. package/.hile-rsc/build-mtie8wg2-ca38f847/client-browser/src-plugin-password-form-3bd63d69e5-CCNUYY62.js +706 -0
  5. package/.hile-rsc/build-mtie8wg2-ca38f847/client-browser/src-plugin-password-form-3bd63d69e5-TRYMJX4F.css +195 -0
  6. package/.hile-rsc/build-mtie8wg2-ca38f847/client-ssr/chunks/chunk-36DACA7C.js +155814 -0
  7. package/.hile-rsc/build-mtie8wg2-ca38f847/client-ssr/src-plugin-page-frame-07c4de56a6-SYE4RRNX.js +441 -0
  8. package/.hile-rsc/build-mtie8wg2-ca38f847/client-ssr/src-plugin-password-form-3bd63d69e5-YVCPJXLL.js +708 -0
  9. package/.hile-rsc/build-mtie8wg2-ca38f847/plugin.json +90 -0
  10. package/.hile-rsc/build-mtie8wg2-ca38f847/server-rsc/index.js +15298 -0
  11. package/.hile-rsc/build-mtie8wg2-ca38f847/styles/4b3193c4f2b317ffed425ccf401342a4bdd6ccfd5f9c42def3315e776f166975.css +385 -0
  12. package/dist/command/index.d.ts +3 -0
  13. package/dist/command/index.d.ts.map +1 -0
  14. package/dist/command/index.js +24 -0
  15. package/dist/command/index.js.map +1 -0
  16. package/dist/config/schema.d.ts +72 -0
  17. package/dist/config/schema.d.ts.map +1 -0
  18. package/dist/config/schema.js +48 -0
  19. package/dist/config/schema.js.map +1 -0
  20. package/dist/email/nodemailer-verification-email-sender.d.ts +11 -0
  21. package/dist/email/nodemailer-verification-email-sender.d.ts.map +1 -0
  22. package/dist/email/nodemailer-verification-email-sender.js +54 -0
  23. package/dist/email/nodemailer-verification-email-sender.js.map +1 -0
  24. package/dist/email/verification-code-rate-limiter.d.ts +14 -0
  25. package/dist/email/verification-code-rate-limiter.d.ts.map +1 -0
  26. package/dist/email/verification-code-rate-limiter.js +27 -0
  27. package/dist/email/verification-code-rate-limiter.js.map +1 -0
  28. package/dist/email/verification-email-sender.d.ts +10 -0
  29. package/dist/email/verification-email-sender.d.ts.map +1 -0
  30. package/dist/email/verification-email-sender.js +2 -0
  31. package/dist/email/verification-email-sender.js.map +1 -0
  32. package/dist/messages/auth-extension/begin.msg.d.ts +3 -0
  33. package/dist/messages/auth-extension/begin.msg.d.ts.map +1 -0
  34. package/dist/messages/auth-extension/begin.msg.js +8 -0
  35. package/dist/messages/auth-extension/begin.msg.js.map +1 -0
  36. package/dist/messages/auth-extension/complete.msg.d.ts +3 -0
  37. package/dist/messages/auth-extension/complete.msg.d.ts.map +1 -0
  38. package/dist/messages/auth-extension/complete.msg.js +8 -0
  39. package/dist/messages/auth-extension/complete.msg.js.map +1 -0
  40. package/dist/migrations/index.d.ts +3 -0
  41. package/dist/migrations/index.d.ts.map +1 -0
  42. package/dist/migrations/index.js +2 -0
  43. package/dist/migrations/index.js.map +1 -0
  44. package/dist/password/attempt-coordinator.d.ts +12 -0
  45. package/dist/password/attempt-coordinator.d.ts.map +1 -0
  46. package/dist/password/attempt-coordinator.js +25 -0
  47. package/dist/password/attempt-coordinator.js.map +1 -0
  48. package/dist/password/credential-lifecycle-outbox.d.ts +42 -0
  49. package/dist/password/credential-lifecycle-outbox.d.ts.map +1 -0
  50. package/dist/password/credential-lifecycle-outbox.js +119 -0
  51. package/dist/password/credential-lifecycle-outbox.js.map +1 -0
  52. package/dist/password/credential-repository.d.ts +41 -0
  53. package/dist/password/credential-repository.d.ts.map +1 -0
  54. package/dist/password/credential-repository.js +64 -0
  55. package/dist/password/credential-repository.js.map +1 -0
  56. package/dist/password/password-authenticator.d.ts +59 -0
  57. package/dist/password/password-authenticator.d.ts.map +1 -0
  58. package/dist/password/password-authenticator.js +184 -0
  59. package/dist/password/password-authenticator.js.map +1 -0
  60. package/dist/password/typeorm-credential-repository.d.ts +15 -0
  61. package/dist/password/typeorm-credential-repository.d.ts.map +1 -0
  62. package/dist/password/typeorm-credential-repository.js +125 -0
  63. package/dist/password/typeorm-credential-repository.js.map +1 -0
  64. package/dist/persistence/entities.d.ts +29 -0
  65. package/dist/persistence/entities.d.ts.map +1 -0
  66. package/dist/persistence/entities.js +49 -0
  67. package/dist/persistence/entities.js.map +1 -0
  68. package/dist/runtime/auth-announcement-publisher.d.ts +18 -0
  69. package/dist/runtime/auth-announcement-publisher.d.ts.map +1 -0
  70. package/dist/runtime/auth-announcement-publisher.js +106 -0
  71. package/dist/runtime/auth-announcement-publisher.js.map +1 -0
  72. package/dist/runtime/password-auth-runtime.d.ts +33 -0
  73. package/dist/runtime/password-auth-runtime.d.ts.map +1 -0
  74. package/dist/runtime/password-auth-runtime.js +218 -0
  75. package/dist/runtime/password-auth-runtime.js.map +1 -0
  76. package/dist/runtime/password-state-store.d.ts +49 -0
  77. package/dist/runtime/password-state-store.d.ts.map +1 -0
  78. package/dist/runtime/password-state-store.js +130 -0
  79. package/dist/runtime/password-state-store.js.map +1 -0
  80. package/dist/service-identity.d.ts +2 -0
  81. package/dist/service-identity.d.ts.map +1 -0
  82. package/dist/service-identity.js +2 -0
  83. package/dist/service-identity.js.map +1 -0
  84. package/dist/services/application.service.d.ts +8 -0
  85. package/dist/services/application.service.d.ts.map +1 -0
  86. package/dist/services/application.service.js +22 -0
  87. package/dist/services/application.service.js.map +1 -0
  88. package/dist/services/config.service.d.ts +63 -0
  89. package/dist/services/config.service.d.ts.map +1 -0
  90. package/dist/services/config.service.js +20 -0
  91. package/dist/services/config.service.js.map +1 -0
  92. package/dist/services/database.service.d.ts +3 -0
  93. package/dist/services/database.service.d.ts.map +1 -0
  94. package/dist/services/database.service.js +23 -0
  95. package/dist/services/database.service.js.map +1 -0
  96. package/dist/services/email.service.d.ts +4 -0
  97. package/dist/services/email.service.d.ts.map +1 -0
  98. package/dist/services/email.service.js +37 -0
  99. package/dist/services/email.service.js.map +1 -0
  100. package/dist/services/logger.service.d.ts +4 -0
  101. package/dist/services/logger.service.d.ts.map +1 -0
  102. package/dist/services/logger.service.js +8 -0
  103. package/dist/services/logger.service.js.map +1 -0
  104. package/dist/services/password-auth.boot.d.ts +3 -0
  105. package/dist/services/password-auth.boot.d.ts.map +1 -0
  106. package/dist/services/password-auth.boot.js +4 -0
  107. package/dist/services/password-auth.boot.js.map +1 -0
  108. package/dist/services/password-runtime.service.d.ts +4 -0
  109. package/dist/services/password-runtime.service.d.ts.map +1 -0
  110. package/dist/services/password-runtime.service.js +135 -0
  111. package/dist/services/password-runtime.service.js.map +1 -0
  112. package/dist/services/redis.service.d.ts +4 -0
  113. package/dist/services/redis.service.d.ts.map +1 -0
  114. package/dist/services/redis.service.js +18 -0
  115. package/dist/services/redis.service.js.map +1 -0
  116. package/package.json +91 -0
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/command/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'node:url';
3
+ import { createRegistryDatabaseDataSource, registerDatabaseCommands, } from '@zlooks.cn/database-command';
4
+ import { runServiceCommand } from '@zlooks.cn/service-command';
5
+ import { passwordAuthMigrations } from '../migrations/index.js';
6
+ import { passwordAuthEntities } from '../persistence/entities.js';
7
+ await runServiceCommand({
8
+ packageRoot: fileURLToPath(new URL('../../', import.meta.url)),
9
+ requiresReactServer: true,
10
+ configure(program) {
11
+ registerDatabaseCommands(program, {
12
+ createDataSource: (envFile) => createRegistryDatabaseDataSource(envFile, {
13
+ commandNamespace: 'cn.zlooks.password.auth.db.command',
14
+ entities: [...passwordAuthEntities],
15
+ }),
16
+ migrations: passwordAuthMigrations,
17
+ });
18
+ },
19
+ }).catch(reportStartupFailure);
20
+ function reportStartupFailure(error) {
21
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
22
+ process.exitCode = 1;
23
+ }
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/command/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EACL,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE,MAAM,iBAAiB,CAAC;IACtB,WAAW,EAAE,aAAa,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9D,mBAAmB,EAAE,IAAI;IACzB,SAAS,CAAC,OAAO;QACf,wBAAwB,CAAC,OAAO,EAAE;YAChC,gBAAgB,EAAE,CAAC,OAAO,EAAE,EAAE,CAC5B,gCAAgC,CAAC,OAAO,EAAE;gBACxC,gBAAgB,EAAE,oCAAoC;gBACtD,QAAQ,EAAE,CAAC,GAAG,oBAAoB,CAAC;aACpC,CAAC;YACJ,UAAU,EAAE,sBAAsB;SACnC,CAAC,CAAC;IACL,CAAC;CACF,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAE/B,SAAS,oBAAoB,CAAC,KAAc;IAC1C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ declare const schema: z.ZodObject<{
3
+ infrastructure: z.ZodObject<{
4
+ postgres: z.ZodObject<{
5
+ host: z.ZodString;
6
+ port: z.ZodNumber;
7
+ username: z.ZodString;
8
+ password: z.ZodString;
9
+ database: z.ZodString;
10
+ ssl: z.ZodBoolean;
11
+ poolSize: z.ZodNumber;
12
+ }, z.core.$strict>;
13
+ redis: z.ZodObject<{
14
+ host: z.ZodString;
15
+ port: z.ZodNumber;
16
+ username: z.ZodOptional<z.ZodString>;
17
+ password: z.ZodOptional<z.ZodString>;
18
+ database: z.ZodNumber;
19
+ keyPrefix: z.ZodString;
20
+ tls: z.ZodBoolean;
21
+ }, z.core.$strict>;
22
+ email: z.ZodObject<{
23
+ smtp: z.ZodObject<{
24
+ host: z.ZodString;
25
+ port: z.ZodNumber;
26
+ secure: z.ZodBoolean;
27
+ username: z.ZodString;
28
+ password: z.ZodString;
29
+ }, z.core.$strict>;
30
+ from: z.ZodObject<{
31
+ name: z.ZodString;
32
+ address: z.ZodEmail;
33
+ }, z.core.$strict>;
34
+ }, z.core.$strict>;
35
+ }, z.core.$strict>;
36
+ discovery: z.ZodObject<{
37
+ instanceId: z.ZodLiteral<"password-auth">;
38
+ generation: z.ZodLiteral<2>;
39
+ leaseMs: z.ZodLiteral<10000>;
40
+ }, z.core.$strict>;
41
+ rsc: z.ZodObject<{
42
+ generation: z.ZodLiteral<2>;
43
+ priority: z.ZodLiteral<100>;
44
+ }, z.core.$strict>;
45
+ password: z.ZodObject<{
46
+ minLength: z.ZodLiteral<12>;
47
+ maxLength: z.ZodLiteral<128>;
48
+ memoryCostKiB: z.ZodLiteral<65536>;
49
+ timeCost: z.ZodLiteral<3>;
50
+ parallelism: z.ZodLiteral<1>;
51
+ maxFailedAttempts: z.ZodLiteral<5>;
52
+ lockoutMs: z.ZodLiteral<900000>;
53
+ }, z.core.$strict>;
54
+ verification: z.ZodObject<{
55
+ hmacKey: z.ZodString;
56
+ codeTtlMs: z.ZodLiteral<600000>;
57
+ resendIntervalMs: z.ZodLiteral<60000>;
58
+ maxSendsPerTransaction: z.ZodLiteral<3>;
59
+ maxSendsPerEmailPerHour: z.ZodLiteral<5>;
60
+ maxVerificationAttempts: z.ZodLiteral<5>;
61
+ }, z.core.$strict>;
62
+ }, z.core.$strict>;
63
+ export interface PasswordAuthConfigSources {
64
+ readonly postgres: unknown;
65
+ readonly redis: unknown;
66
+ readonly email: unknown;
67
+ readonly passwordAuth: unknown;
68
+ }
69
+ export type PasswordAuthConfig = Readonly<z.infer<typeof schema>>;
70
+ export declare function parsePasswordAuthConfig(input: PasswordAuthConfigSources): PasswordAuthConfig;
71
+ export {};
72
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAaV,CAAC;AAUH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AACD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC;AAClE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,yBAAyB,GAAG,kBAAkB,CAmB5F"}
@@ -0,0 +1,48 @@
1
+ import { emailConfigSchema, postgresConfigSchema, redisConfigSchema } from '@zlooks.cn/infrastructure';
2
+ import { deepFreeze, joinKeyPrefix } from '@zlooks.cn/utils';
3
+ import { z } from 'zod';
4
+ const schema = z.strictObject({
5
+ infrastructure: z.strictObject({ postgres: postgresConfigSchema, redis: redisConfigSchema, email: emailConfigSchema }),
6
+ discovery: z.strictObject({ instanceId: z.literal('password-auth'), generation: z.literal(2), leaseMs: z.literal(10_000) }),
7
+ rsc: z.strictObject({ generation: z.literal(2), priority: z.literal(100) }),
8
+ password: z.strictObject({ minLength: z.literal(12), maxLength: z.literal(128), memoryCostKiB: z.literal(65_536), timeCost: z.literal(3), parallelism: z.literal(1), maxFailedAttempts: z.literal(5), lockoutMs: z.literal(900_000) }),
9
+ verification: z.strictObject({
10
+ hmacKey: z.string(),
11
+ codeTtlMs: z.literal(600_000),
12
+ resendIntervalMs: z.literal(60_000),
13
+ maxSendsPerTransaction: z.literal(3),
14
+ maxSendsPerEmailPerHour: z.literal(5),
15
+ maxVerificationAttempts: z.literal(5),
16
+ }),
17
+ });
18
+ const passwordAuthSourceSchema = z.strictObject({
19
+ verificationCodeHmacKey: z.string().regex(/^[A-Za-z0-9+/]{43}=$/).refine((value) => {
20
+ try {
21
+ return Buffer.from(value, 'base64').byteLength === 32;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }, 'Verification code HMAC key must be a base64-encoded 32-byte key'),
27
+ });
28
+ export function parsePasswordAuthConfig(input) {
29
+ const postgres = postgresConfigSchema.parse(input.postgres);
30
+ const redis = redisConfigSchema.parse(input.redis);
31
+ const email = emailConfigSchema.parse(input.email);
32
+ const passwordAuth = passwordAuthSourceSchema.parse(input.passwordAuth);
33
+ return deepFreeze(schema.parse({
34
+ infrastructure: { postgres, redis: { ...redis, keyPrefix: joinKeyPrefix(redis.keyPrefix, 'password-auth') }, email },
35
+ discovery: { instanceId: 'password-auth', generation: 2, leaseMs: 10_000 },
36
+ rsc: { generation: 2, priority: 100 },
37
+ password: { minLength: 12, maxLength: 128, memoryCostKiB: 65_536, timeCost: 3, parallelism: 1, maxFailedAttempts: 5, lockoutMs: 900_000 },
38
+ verification: {
39
+ hmacKey: passwordAuth.verificationCodeHmacKey,
40
+ codeTtlMs: 600_000,
41
+ resendIntervalMs: 60_000,
42
+ maxSendsPerTransaction: 3,
43
+ maxSendsPerEmailPerHour: 5,
44
+ maxVerificationAttempts: 5,
45
+ },
46
+ }));
47
+ }
48
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Transporter } from 'nodemailer';
2
+ import type { EmailConfig } from '@zlooks.cn/infrastructure';
3
+ import type { VerificationEmailSender } from './verification-email-sender.js';
4
+ export declare class NodemailerVerificationEmailSender implements VerificationEmailSender {
5
+ #private;
6
+ private readonly config;
7
+ constructor(config: EmailConfig, transporter?: Transporter);
8
+ send(input: Parameters<VerificationEmailSender['send']>[0]): Promise<void>;
9
+ close(): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=nodemailer-verification-email-sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodemailer-verification-email-sender.d.ts","sourceRoot":"","sources":["../../src/email/nodemailer-verification-email-sender.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE9E,qBAAa,iCAAkC,YAAW,uBAAuB;;IAK5D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAA1C,YAAoC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,WAAW,EAWjF;IAEY,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBtF;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAIlC;CACF"}
@@ -0,0 +1,54 @@
1
+ import nodemailer from 'nodemailer';
2
+ export class NodemailerVerificationEmailSender {
3
+ config;
4
+ #transporter;
5
+ #inFlight = new Set();
6
+ #closing = false;
7
+ constructor(config, transporter) {
8
+ this.config = config;
9
+ this.#transporter = transporter ?? nodemailer.createTransport({
10
+ host: config.smtp.host,
11
+ port: config.smtp.port,
12
+ secure: config.smtp.secure,
13
+ auth: { user: config.smtp.username, pass: config.smtp.password },
14
+ pool: true,
15
+ connectionTimeout: 10_000,
16
+ greetingTimeout: 10_000,
17
+ socketTimeout: 20_000,
18
+ });
19
+ }
20
+ async send(input) {
21
+ if (this.#closing)
22
+ throw new Error('Email delivery is shutting down');
23
+ const purpose = input.intent === 'registration'
24
+ ? '注册账号'
25
+ : input.intent === 'recovery'
26
+ ? '找回密码'
27
+ : '登录账号';
28
+ const operation = Promise.resolve().then(() => this.#transporter.sendMail({
29
+ from: { name: this.config.from.name, address: this.config.from.address },
30
+ to: input.email,
31
+ subject: `ZLOOKS ${purpose}验证码`,
32
+ text: `你的验证码是 ${input.code},${input.expiresInMinutes} 分钟内有效。请勿将验证码告诉他人。`,
33
+ html: `<p>你的验证码是:</p><p style="font-size:28px;font-weight:700;letter-spacing:6px">${input.code}</p><p>${input.expiresInMinutes} 分钟内有效。请勿将验证码告诉他人。</p>`,
34
+ }));
35
+ this.#inFlight.add(operation);
36
+ let result;
37
+ try {
38
+ result = await operation;
39
+ }
40
+ finally {
41
+ this.#inFlight.delete(operation);
42
+ }
43
+ if (!Array.isArray(result.accepted)
44
+ || !result.accepted.some((address) => String(address).toLocaleLowerCase('en-US') === input.email)) {
45
+ throw new Error('SMTP server did not accept the verification email recipient');
46
+ }
47
+ }
48
+ async close() {
49
+ this.#closing = true;
50
+ await Promise.allSettled(this.#inFlight);
51
+ this.#transporter.close();
52
+ }
53
+ }
54
+ //# sourceMappingURL=nodemailer-verification-email-sender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodemailer-verification-email-sender.js","sourceRoot":"","sources":["../../src/email/nodemailer-verification-email-sender.ts"],"names":[],"mappings":"AACA,OAAO,UAAU,MAAM,YAAY,CAAC;AAIpC,MAAM,OAAO,iCAAiC;IAKR,MAAM;IAJjC,YAAY,CAAc;IAC1B,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;IACjD,QAAQ,GAAG,KAAK,CAAC;IAEjB,YAAoC,MAAmB,EAAE,WAAyB;sBAA9C,MAAM;QACxC,IAAI,CAAC,YAAY,GAAG,WAAW,IAAI,UAAU,CAAC,eAAe,CAAC;YAC5D,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI;YACtB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI;YACtB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM;YAC1B,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE;YAChE,IAAI,EAAE,IAAI;YACV,iBAAiB,EAAE,MAAM;YACzB,eAAe,EAAE,MAAM;YACvB,aAAa,EAAE,MAAM;SACtB,CAAC,CAAC;IACL,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,KAAqD;QACrE,IAAI,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,KAAK,cAAc;YAC7C,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,UAAU;gBAC3B,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,MAAM,CAAC;QACb,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC;YACxE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE;YACxE,EAAE,EAAE,KAAK,CAAC,KAAK;YACf,OAAO,EAAE,UAAU,OAAO,KAAK;YAC/B,IAAI,EAAE,UAAU,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,gBAAgB,oBAAoB;YACxE,IAAI,EAAE,8EAA8E,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC,gBAAgB,wBAAwB;SACvJ,CAAC,CAAC,CAAC;QACJ,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC9B,IAAI,MAAiC,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,SAAS,CAAC;QAC3B,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;eAC9B,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,iBAAiB,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7G,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,KAAK;QAChB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,MAAM,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzC,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ import type { AuthenticationIntent } from '@zlooks.cn/auth-extension-contract';
2
+ import type { Redis } from 'ioredis';
3
+ export interface VerificationCodeRateLimiter {
4
+ consume(email: string, intent: Extract<AuthenticationIntent, 'login' | 'registration' | 'recovery'>): Promise<boolean>;
5
+ }
6
+ export declare class RedisVerificationCodeRateLimiter implements VerificationCodeRateLimiter {
7
+ #private;
8
+ private readonly limit;
9
+ private readonly hmacKey;
10
+ constructor(redis: Redis, limit: number, hmacKey: string);
11
+ consume(email: string, intent: Extract<AuthenticationIntent, 'login' | 'registration' | 'recovery'>): Promise<boolean>;
12
+ }
13
+ export declare function verificationRateLimitIdentity(email: string, hmacKey: string): string;
14
+ //# sourceMappingURL=verification-code-rate-limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-code-rate-limiter.d.ts","sourceRoot":"","sources":["../../src/email/verification-code-rate-limiter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGrC,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,oBAAoB,EAAE,OAAO,GAAG,cAAc,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxH;AAED,qBAAa,gCAAiC,YAAW,2BAA2B;;IAKhF,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,YACE,KAAK,EAAE,KAAK,EACK,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAGjC;IAEY,OAAO,CAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,OAAO,CAAC,oBAAoB,EAAE,OAAO,GAAG,cAAc,GAAG,UAAU,CAAC,GAC3E,OAAO,CAAC,OAAO,CAAC,CAOlB;CACF;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKpF"}
@@ -0,0 +1,27 @@
1
+ import { defineLimit, RedisRateLimiter } from '@hile/redis-rate-limit';
2
+ import { createHmac } from 'node:crypto';
3
+ export class RedisVerificationCodeRateLimiter {
4
+ limit;
5
+ hmacKey;
6
+ #limiter;
7
+ constructor(redis, limit, hmacKey) {
8
+ this.limit = limit;
9
+ this.hmacKey = hmacKey;
10
+ this.#limiter = new RedisRateLimiter(redis, { prefix: 'verification-rate-limit:' });
11
+ }
12
+ async consume(email, intent) {
13
+ const result = await this.#limiter.consume(defineLimit('email-code:{intent:string}:{email:string}', {
14
+ algorithm: 'sliding-window',
15
+ limit: this.limit,
16
+ window: 3_600_000,
17
+ }), { intent, email: verificationRateLimitIdentity(email, this.hmacKey) });
18
+ return result.allowed;
19
+ }
20
+ }
21
+ export function verificationRateLimitIdentity(email, hmacKey) {
22
+ return createHmac('sha256', Buffer.from(hmacKey, 'base64'))
23
+ .update('email-verification-rate-limit\0')
24
+ .update(email)
25
+ .digest('hex');
26
+ }
27
+ //# sourceMappingURL=verification-code-rate-limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-code-rate-limiter.js","sourceRoot":"","sources":["../../src/email/verification-code-rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAGvE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAMzC,MAAM,OAAO,gCAAgC;IAKxB,KAAK;IACL,OAAO;IALjB,QAAQ,CAAmB;IAEpC,YACE,KAAY,EACK,KAAa,EACb,OAAe;qBADf,KAAK;uBACL,OAAO;QAExB,IAAI,CAAC,QAAQ,GAAG,IAAI,gBAAgB,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC,CAAC;IACtF,CAAC;IAEM,KAAK,CAAC,OAAO,CAClB,KAAa,EACb,MAA4E;QAE5E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,2CAA2C,EAAE;YAClG,SAAS,EAAE,gBAAgB;YAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,MAAM,EAAE,SAAS;SAClB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,6BAA6B,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC3E,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;CACF;AAED,MAAM,UAAU,6BAA6B,CAAC,KAAa,EAAE,OAAe;IAC1E,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;SACxD,MAAM,CAAC,iCAAiC,CAAC;SACzC,MAAM,CAAC,KAAK,CAAC;SACb,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { AuthenticationIntent } from '@zlooks.cn/auth-extension-contract';
2
+ export interface VerificationEmailSender {
3
+ send(input: Readonly<{
4
+ email: string;
5
+ code: string;
6
+ intent: Extract<AuthenticationIntent, 'login' | 'registration' | 'recovery'>;
7
+ expiresInMinutes: number;
8
+ }>): Promise<void>;
9
+ }
10
+ //# sourceMappingURL=verification-email-sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-email-sender.d.ts","sourceRoot":"","sources":["../../src/email/verification-email-sender.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAE/E,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,OAAO,CAAC,oBAAoB,EAAE,OAAO,GAAG,cAAc,GAAG,UAAU,CAAC,CAAC;QAC7E,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=verification-email-sender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification-email-sender.js","sourceRoot":"","sources":["../../src/email/verification-email-sender.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ declare const _default: import("@hile/message-loader").MessageRegisterProps<any, import("@hile/micro").MicroMessageHandlerExtras>;
2
+ export default _default;
3
+ //# sourceMappingURL=begin.msg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"begin.msg.d.ts","sourceRoot":"","sources":["../../../src/messages/auth-extension/begin.msg.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { loadService } from '@hile/core';
2
+ import { defineMicroMessage } from '@hile/micro';
3
+ import passwordRuntimeService from '../../services/password-runtime.service.js';
4
+ export default defineMicroMessage(async ({ data }) => {
5
+ const runtime = await loadService(passwordRuntimeService);
6
+ return runtime.begin(data);
7
+ });
8
+ //# sourceMappingURL=begin.msg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"begin.msg.js","sourceRoot":"","sources":["../../../src/messages/auth-extension/begin.msg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,sBAAsB,MAAM,4CAA4C,CAAC;AAEhF,eAAe,kBAAkB,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;IACnD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAC1D,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ declare const _default: import("@hile/message-loader").MessageRegisterProps<any, import("@hile/micro").MicroMessageHandlerExtras>;
2
+ export default _default;
3
+ //# sourceMappingURL=complete.msg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"complete.msg.d.ts","sourceRoot":"","sources":["../../../src/messages/auth-extension/complete.msg.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { loadService } from '@hile/core';
2
+ import { defineMicroMessage } from '@hile/micro';
3
+ import passwordRuntimeService from '../../services/password-runtime.service.js';
4
+ export default defineMicroMessage(async ({ data }) => {
5
+ const runtime = await loadService(passwordRuntimeService);
6
+ return runtime.complete(data);
7
+ });
8
+ //# sourceMappingURL=complete.msg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"complete.msg.js","sourceRoot":"","sources":["../../../src/messages/auth-extension/complete.msg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,sBAAsB,MAAM,4CAA4C,CAAC;AAEhF,eAAe,kBAAkB,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;IACnD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,sBAAsB,CAAC,CAAC;IAC1D,OAAO,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { DatabaseMigration } from '@zlooks.cn/database-command';
2
+ export declare const passwordAuthMigrations: readonly DatabaseMigration[];
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAErE,eAAO,MAAM,sBAAsB,EAAE,SAAS,iBAAiB,EAAO,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const passwordAuthMigrations = [];
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,sBAAsB,GAAiC,EAAE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Redis } from 'ioredis';
2
+ export interface PasswordAttemptCoordinator {
3
+ run<T>(identifier: string, operation: () => Promise<T>): Promise<T>;
4
+ }
5
+ export declare class RedisPasswordAttemptCoordinator implements PasswordAttemptCoordinator {
6
+ #private;
7
+ private readonly hmacKey;
8
+ constructor(redis: Redis, hmacKey: string);
9
+ run<T>(identifier: string, operation: () => Promise<T>): Promise<T>;
10
+ }
11
+ export declare function passwordAttemptIdentity(identifier: string, hmacKey: string): string;
12
+ //# sourceMappingURL=attempt-coordinator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attempt-coordinator.d.ts","sourceRoot":"","sources":["../../src/password/attempt-coordinator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,0BAA0B;IACzC,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACrE;AAED,qBAAa,+BAAgC,YAAW,0BAA0B;;IAG/C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAAzD,YAAmB,KAAK,EAAE,KAAK,EAAmB,OAAO,EAAE,MAAM,EAEhE;IAEY,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO/E;CACF;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKnF"}
@@ -0,0 +1,25 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { RedisLock } from '@hile/redis-lock';
3
+ export class RedisPasswordAttemptCoordinator {
4
+ hmacKey;
5
+ #locks;
6
+ constructor(redis, hmacKey) {
7
+ this.hmacKey = hmacKey;
8
+ this.#locks = new RedisLock(redis, { prefix: 'locks:', defaultTtl: 30_000 });
9
+ }
10
+ async run(identifier, operation) {
11
+ const key = passwordAttemptIdentity(identifier, this.hmacKey);
12
+ return this.#locks.withLock(`password-attempt:${key}`, {
13
+ wait: 5_000,
14
+ ttl: 30_000,
15
+ renew: true,
16
+ }, async () => operation());
17
+ }
18
+ }
19
+ export function passwordAttemptIdentity(identifier, hmacKey) {
20
+ return createHmac('sha256', Buffer.from(hmacKey, 'base64'))
21
+ .update('password-attempt-lock\0')
22
+ .update(identifier)
23
+ .digest('hex');
24
+ }
25
+ //# sourceMappingURL=attempt-coordinator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attempt-coordinator.js","sourceRoot":"","sources":["../../src/password/attempt-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAO7C,MAAM,OAAO,+BAA+B;IAGQ,OAAO;IAFhD,MAAM,CAAY;IAE3B,YAAmB,KAAY,EAAmB,OAAe;uBAAf,OAAO;QACvD,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/E,CAAC;IAEM,KAAK,CAAC,GAAG,CAAI,UAAkB,EAAE,SAA2B;QACjE,MAAM,GAAG,GAAG,uBAAuB,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,GAAG,EAAE,EAAE;YACrD,IAAI,EAAE,KAAK;YACX,GAAG,EAAE,MAAM;YACX,KAAK,EAAE,IAAI;SACZ,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,UAAU,uBAAuB,CAAC,UAAkB,EAAE,OAAe;IACzE,OAAO,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;SACxD,MAAM,CAAC,yBAAyB,CAAC;SACjC,MAAM,CAAC,UAAU,CAAC;SAClB,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { type ExecutionContext } from '@hile/context';
2
+ import type { Redis } from 'ioredis';
3
+ import { type DataSource } from 'typeorm';
4
+ import { type PasswordCredentialLifecycleOutboxRow } from '../persistence/entities.js';
5
+ export type PendingCredentialLifecycleEvent = Readonly<Pick<PasswordCredentialLifecycleOutboxRow, 'transactionId' | 'kind' | 'subject' | 'identifier' | 'occurredAt' | 'attempts'>>;
6
+ export interface PasswordCredentialLifecycleOutboxStore {
7
+ readDue(now: Date, limit: number): Promise<readonly PendingCredentialLifecycleEvent[]>;
8
+ acknowledge(transactionId: string): Promise<void>;
9
+ recordFailure(transactionId: string, attempts: number, nextAttemptAt: Date): Promise<void>;
10
+ }
11
+ export interface CredentialLifecycleMicroCaller {
12
+ call(namespace: string, operation: string, data: unknown, options: Readonly<{
13
+ context: ExecutionContext;
14
+ timeout: number;
15
+ retries: number;
16
+ signal: AbortSignal;
17
+ }>): Promise<unknown>;
18
+ }
19
+ export declare class TypeOrmPasswordCredentialLifecycleOutboxStore implements PasswordCredentialLifecycleOutboxStore {
20
+ private readonly dataSource;
21
+ constructor(dataSource: DataSource);
22
+ readDue(now: Date, limit: number): Promise<readonly PendingCredentialLifecycleEvent[]>;
23
+ acknowledge(transactionId: string): Promise<void>;
24
+ recordFailure(transactionId: string, attempts: number, nextAttemptAt: Date): Promise<void>;
25
+ }
26
+ export declare class PasswordCredentialLifecyclePublisher {
27
+ #private;
28
+ private readonly options;
29
+ constructor(options: {
30
+ readonly application: CredentialLifecycleMicroCaller;
31
+ readonly redis: Redis;
32
+ readonly store: PasswordCredentialLifecycleOutboxStore;
33
+ readonly clock?: () => Date;
34
+ readonly onError?: (error: unknown, event?: PendingCredentialLifecycleEvent) => void;
35
+ });
36
+ start(): void;
37
+ close(): Promise<void>;
38
+ flush(): Promise<void>;
39
+ private runFlush;
40
+ private publish;
41
+ }
42
+ //# sourceMappingURL=credential-lifecycle-outbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-lifecycle-outbox.d.ts","sourceRoot":"","sources":["../../src/password/credential-lifecycle-outbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQ9E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,EAEL,KAAK,oCAAoC,EAC1C,MAAM,4BAA4B,CAAC;AAEpC,MAAM,MAAM,+BAA+B,GAAG,QAAQ,CAAC,IAAI,CACzD,oCAAoC,EACpC,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,GAAG,YAAY,GAAG,UAAU,CAChF,CAAC,CAAC;AAEH,MAAM,WAAW,sCAAsC;IACrD,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,+BAA+B,EAAE,CAAC,CAAC;IACvF,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5F;AAED,MAAM,WAAW,8BAA8B;IAC7C,IAAI,CACF,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,QAAQ,CAAC;QAChB,OAAO,EAAE,gBAAgB,CAAC;QAC1B,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,WAAW,CAAC;KACrB,CAAC,GACD,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,qBAAa,6CACb,YAAW,sCAAsC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAA9C,YAAoC,UAAU,EAAE,UAAU,EAAI;IAEjD,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,+BAA+B,EAAE,CAAC,CAclG;IAEY,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7D;IAEY,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAKtG;CACF;AAED,qBAAa,oCAAoC;;IAO5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAA3C,YAAoC,OAAO,EAAE;QAC3C,QAAQ,CAAC,WAAW,EAAE,8BAA8B,CAAC;QACrD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QACtB,QAAQ,CAAC,KAAK,EAAE,sCAAsC,CAAC;QACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;QAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,+BAA+B,KAAK,IAAI,CAAC;KACtF,EAEA;IAEM,KAAK,IAAI,IAAI,CAanB;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAKlC;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CASlC;YAEa,QAAQ;YAeR,OAAO;CAmCtB"}
@@ -0,0 +1,119 @@
1
+ import { createExecutionContext } from '@hile/context';
2
+ import { Scheduler } from '@hile/schedule';
3
+ import { parseUserCredentialLifecycleResponse, USER_CREDENTIAL_LIFECYCLE_OPERATION, USER_SERVICE_NAMESPACE, } from '@zlooks.cn/user-shared';
4
+ import { LessThanOrEqual } from 'typeorm';
5
+ import { PasswordCredentialLifecycleOutboxEntity, } from '../persistence/entities.js';
6
+ export class TypeOrmPasswordCredentialLifecycleOutboxStore {
7
+ dataSource;
8
+ constructor(dataSource) {
9
+ this.dataSource = dataSource;
10
+ }
11
+ async readDue(now, limit) {
12
+ return this.dataSource.getRepository(PasswordCredentialLifecycleOutboxEntity).find({
13
+ where: { nextAttemptAt: LessThanOrEqual(now) },
14
+ order: { createdAt: 'ASC' },
15
+ take: limit,
16
+ select: {
17
+ transactionId: true,
18
+ kind: true,
19
+ subject: true,
20
+ identifier: true,
21
+ occurredAt: true,
22
+ attempts: true,
23
+ },
24
+ });
25
+ }
26
+ async acknowledge(transactionId) {
27
+ await this.dataSource.getRepository(PasswordCredentialLifecycleOutboxEntity).delete({ transactionId });
28
+ }
29
+ async recordFailure(transactionId, attempts, nextAttemptAt) {
30
+ await this.dataSource.getRepository(PasswordCredentialLifecycleOutboxEntity).update({ transactionId }, { attempts, nextAttemptAt });
31
+ }
32
+ }
33
+ export class PasswordCredentialLifecyclePublisher {
34
+ options;
35
+ #scheduler = new Scheduler();
36
+ #clock;
37
+ #inFlight = new Set();
38
+ #controllers = new Set();
39
+ #closing = false;
40
+ constructor(options) {
41
+ this.options = options;
42
+ this.#clock = options.clock ?? (() => new Date());
43
+ }
44
+ start() {
45
+ this.#scheduler.add('credential-lifecycle-outbox', '*/2 * * * * *', async () => {
46
+ await this.flush();
47
+ }, {
48
+ distributed: {
49
+ redis: this.options.redis,
50
+ ttl: 30_000,
51
+ namespace: 'zlooks.password-auth',
52
+ policy: 'skip-if-locked',
53
+ renew: true,
54
+ },
55
+ onError: (error) => this.options.onError?.(error),
56
+ });
57
+ }
58
+ async close() {
59
+ this.#closing = true;
60
+ this.#scheduler.stop();
61
+ for (const controller of this.#controllers)
62
+ controller.abort();
63
+ await Promise.allSettled(this.#inFlight);
64
+ }
65
+ async flush() {
66
+ if (this.#closing)
67
+ return;
68
+ const operation = this.runFlush();
69
+ this.#inFlight.add(operation);
70
+ try {
71
+ await operation;
72
+ }
73
+ finally {
74
+ this.#inFlight.delete(operation);
75
+ }
76
+ }
77
+ async runFlush() {
78
+ const controller = new AbortController();
79
+ this.#controllers.add(controller);
80
+ try {
81
+ const events = await this.options.store.readDue(this.#clock(), 20);
82
+ for (let offset = 0; offset < events.length && !controller.signal.aborted; offset += 5) {
83
+ await Promise.all(events.slice(offset, offset + 5).map((event) => this.publish(event, controller.signal)));
84
+ }
85
+ }
86
+ finally {
87
+ this.#controllers.delete(controller);
88
+ }
89
+ }
90
+ async publish(event, signal) {
91
+ try {
92
+ const payload = {
93
+ protocol: 'zlooks.user.credential-lifecycle',
94
+ kind: event.kind,
95
+ authenticationTransactionId: event.transactionId,
96
+ extensionId: 'password',
97
+ methodId: 'password',
98
+ subject: event.subject,
99
+ email: event.identifier,
100
+ occurredAt: event.occurredAt.toISOString(),
101
+ };
102
+ parseUserCredentialLifecycleResponse(await this.options.application.call(USER_SERVICE_NAMESPACE, USER_CREDENTIAL_LIFECYCLE_OPERATION, payload, {
103
+ context: createExecutionContext({}),
104
+ timeout: 5_000,
105
+ retries: 0,
106
+ signal,
107
+ }));
108
+ await this.options.store.acknowledge(event.transactionId);
109
+ }
110
+ catch (error) {
111
+ const attempts = event.attempts + 1;
112
+ const delayMs = Math.min(60_000, 1_000 * (2 ** Math.min(attempts, 6)));
113
+ await this.options.store.recordFailure(event.transactionId, attempts, new Date(this.#clock().getTime() + delayMs));
114
+ if (!signal.aborted)
115
+ this.options.onError?.(error, event);
116
+ }
117
+ }
118
+ }
119
+ //# sourceMappingURL=credential-lifecycle-outbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-lifecycle-outbox.js","sourceRoot":"","sources":["../../src/password/credential-lifecycle-outbox.ts"],"names":[],"mappings":"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"}