@gasboost/auth 0.1.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 (68) hide show
  1. package/README.md +484 -0
  2. package/dist/AppsScriptAuth.d.ts +51 -0
  3. package/dist/AppsScriptAuth.js +77 -0
  4. package/dist/AuthPattern.d.ts +5 -0
  5. package/dist/AuthPattern.js +4 -0
  6. package/dist/api/AppsScriptAuthPassword.d.ts +30 -0
  7. package/dist/api/AppsScriptAuthPassword.js +64 -0
  8. package/dist/api/AppsScriptAuthSession.d.ts +10 -0
  9. package/dist/api/AppsScriptAuthSession.js +17 -0
  10. package/dist/api/AppsScriptAuthSignIn.d.ts +20 -0
  11. package/dist/api/AppsScriptAuthSignIn.js +34 -0
  12. package/dist/api/AppsScriptAuthSignOut.d.ts +7 -0
  13. package/dist/api/AppsScriptAuthSignOut.js +9 -0
  14. package/dist/api/AppsScriptAuthSignUp.d.ts +20 -0
  15. package/dist/api/AppsScriptAuthSignUp.js +35 -0
  16. package/dist/api/SignIn.d.ts +19 -0
  17. package/dist/api/SignIn.js +26 -0
  18. package/dist/api/SignUp.d.ts +20 -0
  19. package/dist/api/SignUp.js +25 -0
  20. package/dist/authentication/AppsScriptAuthentication.d.ts +24 -0
  21. package/dist/authentication/AppsScriptAuthentication.js +47 -0
  22. package/dist/authentication/Authentication.d.ts +4 -0
  23. package/dist/authentication/Authentication.js +1 -0
  24. package/dist/authentication/EmailPasswordAuthentication.d.ts +42 -0
  25. package/dist/authentication/EmailPasswordAuthentication.js +75 -0
  26. package/dist/domain/Account.d.ts +11 -0
  27. package/dist/domain/Account.js +10 -0
  28. package/dist/domain/Errors.d.ts +3 -0
  29. package/dist/domain/Errors.js +5 -0
  30. package/dist/domain/Password.d.ts +15 -0
  31. package/dist/domain/Password.js +54 -0
  32. package/dist/domain/PasswordCredential.d.ts +25 -0
  33. package/dist/domain/PasswordCredential.js +57 -0
  34. package/dist/domain/PasswordReset.d.ts +25 -0
  35. package/dist/domain/PasswordReset.js +44 -0
  36. package/dist/domain/PasswordResetToken.d.ts +6 -0
  37. package/dist/domain/PasswordResetToken.js +17 -0
  38. package/dist/domain/Session.d.ts +14 -0
  39. package/dist/domain/Session.js +15 -0
  40. package/dist/domain/User.d.ts +13 -0
  41. package/dist/domain/User.js +13 -0
  42. package/dist/factory/sessionStorageFactory.d.ts +6 -0
  43. package/dist/factory/sessionStorageFactory.js +11 -0
  44. package/dist/identity/AppsScriptIdentity.d.ts +8 -0
  45. package/dist/identity/AppsScriptIdentity.js +8 -0
  46. package/dist/identity/EmailPasswordIdentity.d.ts +13 -0
  47. package/dist/identity/EmailPasswordIdentity.js +19 -0
  48. package/dist/identity/ProviderIdentity.d.ts +5 -0
  49. package/dist/identity/ProviderIdentity.js +1 -0
  50. package/dist/index.d.ts +11 -0
  51. package/dist/index.js +10 -0
  52. package/dist/registration/AppsScriptRegistration.d.ts +13 -0
  53. package/dist/registration/AppsScriptRegistration.js +46 -0
  54. package/dist/registration/EmailPasswordRegistration.d.ts +16 -0
  55. package/dist/registration/EmailPasswordRegistration.js +41 -0
  56. package/dist/registration/Registration.d.ts +4 -0
  57. package/dist/registration/Registration.js +1 -0
  58. package/dist/schema/AuthSchema.d.ts +64 -0
  59. package/dist/schema/AuthSchema.js +35 -0
  60. package/dist/storage/AppsScriptAuthRepository.d.ts +17 -0
  61. package/dist/storage/AppsScriptAuthRepository.js +1 -0
  62. package/dist/storage/AppsScriptCacheSessionStorage.d.ts +10 -0
  63. package/dist/storage/AppsScriptCacheSessionStorage.js +30 -0
  64. package/dist/storage/AppsScriptPropertiesSessionStorage.d.ts +11 -0
  65. package/dist/storage/AppsScriptPropertiesSessionStorage.js +51 -0
  66. package/dist/storage/AppsScriptSessionStorage.d.ts +7 -0
  67. package/dist/storage/AppsScriptSessionStorage.js +1 -0
  68. package/package.json +55 -0
package/README.md ADDED
@@ -0,0 +1,484 @@
1
+ # @gasboost/auth
2
+
3
+ Google Apps Script 向けの認証ライブラリです。
4
+
5
+ `@gasboost/auth` は、GAS Web App の公開設定とは独立して、アプリケーション上の User / Account / Identity / Session を扱います。
6
+
7
+ 初期実装では以下の認証方式を提供します。
8
+
9
+ - Email / Password
10
+ - Apps Script Active User
11
+
12
+ また、Session の保存先として以下を利用できます。
13
+
14
+ - `CacheService`
15
+ - `PropertiesService`
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pnpm add @gasboost/auth
21
+ ```
22
+
23
+ npm:
24
+
25
+ ```bash
26
+ npm install @gasboost/auth
27
+ ```
28
+
29
+ ## Concepts
30
+
31
+ 認証 Identity と Application User は直接同一視しません。
32
+
33
+ ```text
34
+ ProviderIdentity
35
+
36
+ Account
37
+
38
+ User
39
+
40
+ Session
41
+ ```
42
+
43
+ 1つの User に複数の Account を紐付けることができます。
44
+
45
+ ```text
46
+ User
47
+ ├─ Account(emailPassword)
48
+ └─ Account(appsScript)
49
+ ```
50
+
51
+ Identity は `(provider, accountId)` の組み合わせで識別します。
52
+
53
+ そのため、同じメールアドレスでも認証方式が異なれば別の Identity として扱えます。
54
+
55
+ ```text
56
+ (emailPassword, user@example.com)
57
+ (appsScript, user@example.com)
58
+ ```
59
+
60
+ ## Repository
61
+
62
+ `@gasboost/auth` は特定の Database / ORM に依存しません。
63
+
64
+ 利用側で `AppsScriptAuthRepository` を実装して注入します。
65
+
66
+ ```ts
67
+ import type { AppsScriptAuthRepository } from "@gasboost/auth";
68
+
69
+ const repository: AppsScriptAuthRepository = {
70
+ account: {
71
+ async findByIdentity(provider, identifier) {
72
+ // Account を検索
73
+ return null;
74
+ },
75
+ },
76
+
77
+ user: {
78
+ async find(id) {
79
+ // User を検索
80
+ return null;
81
+ },
82
+
83
+ async create(user) {
84
+ // User + Account を保存
85
+ return user;
86
+ },
87
+ },
88
+ };
89
+ ```
90
+
91
+ 将来的に SheetORM 向け adapter は auth-core とは別 package として提供する想定です。
92
+
93
+ ## Initialize
94
+
95
+ `AppsScriptAuth` に Repository、GAS runtime、Session 設定、認証方式の設定を渡します。
96
+
97
+ ```ts
98
+ import { AppsScriptAuth } from "@gasboost/auth";
99
+
100
+ const pepper =
101
+ PropertiesService.getScriptProperties().getProperty("AUTH_PEPPER") ?? "";
102
+
103
+ const auth = new AppsScriptAuth({
104
+ repository,
105
+
106
+ runtime: {
107
+ utilities: Utilities,
108
+ session: Session,
109
+ cacheService: CacheService,
110
+ propertiesService: PropertiesService,
111
+ },
112
+
113
+ session: {
114
+ storageType: "cache",
115
+ expiresIn: 60 * 20,
116
+ },
117
+
118
+ emailPassword: {
119
+ enabled: true,
120
+ isSignupEnabled: true,
121
+ pepper,
122
+ },
123
+
124
+ appsScript: {
125
+ enabled: true,
126
+ isSignupEnabled: true,
127
+ },
128
+ });
129
+ ```
130
+
131
+ `runtime` を constructor injection するため、auth-core 内部では GAS global を直接参照しません。
132
+
133
+ これにより、テスト環境では `@gasboost/fake-core` / `@gasboost/fake-node` などの fake implementation を注入できます。
134
+
135
+ ## Session Storage
136
+
137
+ ### CacheService
138
+
139
+ ```ts
140
+ session: {
141
+ storageType: "cache",
142
+ expiresIn: 60 * 20,
143
+ }
144
+ ```
145
+
146
+ CacheService の TTL を利用して Session を保持します。
147
+
148
+ 低レイテンシな Session Storage として利用できます。
149
+
150
+ ### PropertiesService
151
+
152
+ ```ts
153
+ session: {
154
+ storageType: "properties",
155
+ expiresIn: 60 * 20,
156
+ }
157
+ ```
158
+
159
+ PropertiesService では Session を以下の key 形式で保存します。
160
+
161
+ ```text
162
+ session:<sessionId>
163
+ ```
164
+
165
+ そのため、同じ Properties に保存された `API_KEY` などの別データとは分離されます。
166
+
167
+ 期限切れ Session は `cleanup()` で削除できます。
168
+
169
+ ## Email / Password Sign Up
170
+
171
+ ```ts
172
+ const { user, session } = await auth.signUp.email({
173
+ name: "Taro",
174
+ email: "user@example.com",
175
+ password: "password",
176
+ });
177
+ ```
178
+
179
+ 処理フロー:
180
+
181
+ ```text
182
+ email identity の重複確認
183
+
184
+ password hash
185
+
186
+ EmailPasswordIdentity
187
+
188
+ Account
189
+
190
+ User
191
+
192
+ Session 発行
193
+ ```
194
+
195
+ ## Email / Password Sign In
196
+
197
+ ```ts
198
+ const { user, session } = await auth.signIn.email({
199
+ email: "user@example.com",
200
+ password: "password",
201
+ });
202
+ ```
203
+
204
+ 処理フロー:
205
+
206
+ ```text
207
+ email
208
+
209
+ Account 検索
210
+
211
+ password verify
212
+
213
+ User 検索
214
+
215
+ Session 発行
216
+ ```
217
+
218
+ ## Apps Script Sign Up
219
+
220
+ Apps Script 認証では `Session.getActiveUser().getEmail()` を Identity として利用します。
221
+
222
+ ```ts
223
+ const { user, session } = await auth.signUp.appsScript({
224
+ name: "Taro",
225
+ });
226
+ ```
227
+
228
+ Active User の email を取得できない場合は登録できません。
229
+
230
+ ## Apps Script Sign In
231
+
232
+ ```ts
233
+ const { user, session } = await auth.signIn.appsScript({});
234
+ ```
235
+
236
+ 処理フロー:
237
+
238
+ ```text
239
+ Session.getActiveUser().getEmail()
240
+
241
+ AppsScriptIdentity
242
+
243
+ Account 検索
244
+
245
+ User 検索
246
+
247
+ Session 発行
248
+ ```
249
+
250
+ ## Session
251
+
252
+ ### Get Session
253
+
254
+ ```ts
255
+ const session = await auth.session.get(sessionId);
256
+
257
+ if (!session) {
258
+ // Session が存在しない、または期限切れ
259
+ }
260
+ ```
261
+
262
+ Session は以下の情報を持ちます。
263
+
264
+ ```ts
265
+ session.id;
266
+ session.userId;
267
+ session.createdAt;
268
+ session.expiresAt;
269
+ ```
270
+
271
+ ### Cleanup Expired Sessions
272
+
273
+ ```ts
274
+ await auth.session.cleanup();
275
+ ```
276
+
277
+ `PropertiesService` 使用時は期限切れ Session を削除します。
278
+
279
+ `CacheService` は自身で expiration を管理するため、cleanup は no-op です。
280
+
281
+ ## Sign Out
282
+
283
+ ```ts
284
+ await auth.signOut.execute(sessionId);
285
+ ```
286
+
287
+ 対象 Session を SessionStorage から削除します。
288
+
289
+ ## Configuration
290
+
291
+ ### Email / Password
292
+
293
+ ```ts
294
+ emailPassword: {
295
+ enabled: true,
296
+ isSignupEnabled: true,
297
+ pepper: "...",
298
+ }
299
+ ```
300
+
301
+ `enabled` が `true` の場合、`pepper` は必須です。
302
+
303
+ 空文字または空白だけの pepper を指定すると `AppsScriptAuth` の初期化時に失敗します。
304
+
305
+ ```ts
306
+ new AppsScriptAuth({
307
+ // ...
308
+ emailPassword: {
309
+ enabled: true,
310
+ pepper: "",
311
+ },
312
+ });
313
+ ```
314
+
315
+ ```text
316
+ Pepper is required when email and password authentication is enabled
317
+ ```
318
+
319
+ `isSignupEnabled` を `false` にすると Sign In は許可したまま Sign Up だけを無効化できます。
320
+
321
+ ```ts
322
+ emailPassword: {
323
+ enabled: true,
324
+ isSignupEnabled: false,
325
+ pepper: "...",
326
+ }
327
+ ```
328
+
329
+ ### Apps Script
330
+
331
+ ```ts
332
+ appsScript: {
333
+ enabled: true,
334
+ isSignupEnabled: true,
335
+ }
336
+ ```
337
+
338
+ こちらも Sign In と Sign Up を個別に制御できます。
339
+
340
+ ## Password
341
+
342
+ Email / Password 認証では以下を利用します。
343
+
344
+ - per-password salt
345
+ - application pepper
346
+ - iterations
347
+ - HMAC-SHA256 を利用した反復処理
348
+
349
+ 現在の default iterations は `300` です。
350
+
351
+ pepper は User / Account の保存先とは分離し、Script Properties などで管理することを推奨します。
352
+
353
+ ```ts
354
+ const pepper =
355
+ PropertiesService.getScriptProperties().getProperty("AUTH_PEPPER");
356
+ ```
357
+
358
+ ## `@gasboost/app` との連携
359
+
360
+ `AppsScriptAuth` は、RPC 登録用の handler collection を `auth.handlers` として公開します。
361
+
362
+ `@gasboost/app` の `.calls()` にそのまま渡すことで、認証用 RPC を一括登録できます。
363
+
364
+ ```ts
365
+ import { AppsScript } from "@gasboost/app";
366
+ import { AppsScriptAuth } from "@gasboost/auth";
367
+
368
+ const auth = new AppsScriptAuth({
369
+ repository,
370
+ runtime,
371
+ session: {
372
+ storageType: "cache",
373
+ },
374
+ emailPassword: {
375
+ enabled: true,
376
+ pepper: "your-secret-pepper",
377
+ },
378
+ appsScript: {
379
+ enabled: true,
380
+ },
381
+ });
382
+
383
+ const app = new AppsScript().calls(auth.handlers);
384
+
385
+ export default app;
386
+ ```
387
+
388
+ 以下の RPC が登録されます。
389
+
390
+ - `signInEmail`
391
+ - `signInAppsScript`
392
+ - `signUpEmail`
393
+ - `signUpAppsScript`
394
+ - `getSession`
395
+ - `signOut`
396
+
397
+ `InferAppsScript` を利用するクライアントでは、これらの RPC がそのまま型推論されます。
398
+
399
+ ```ts
400
+ client.signInEmail({
401
+ email: "user@example.com",
402
+ password: "password",
403
+ });
404
+
405
+ client.getSession("session-id");
406
+
407
+ client.signOut("session-id");
408
+ ```
409
+
410
+ RPC endpoint 名は `@gasboost/auth` 側で管理されるため、利用側で個別に `.call()` を記述したり、endpoint 名を重複定義したりする必要はありません。
411
+
412
+ ## Testing
413
+
414
+ GAS API を利用するテストでは `gasboost/fake` を使用できます。
415
+
416
+ ```bash
417
+ pnpm add -D \
418
+ @gasboost/fake-core \
419
+ @gasboost/fake-node
420
+ ```
421
+
422
+ 例:
423
+
424
+ ```ts
425
+ import {
426
+ InMemoryCacheService,
427
+ InMemoryPropertiesService,
428
+ InMemorySession,
429
+ } from "@gasboost/fake-core";
430
+
431
+ import { NodeUtilities } from "@gasboost/fake-node";
432
+ ```
433
+
434
+ `AppsScriptAuth` の runtime に fake implementation をそのまま注入できます。
435
+
436
+ ```ts
437
+ const auth = new AppsScriptAuth({
438
+ repository,
439
+
440
+ runtime: {
441
+ utilities: new NodeUtilities(),
442
+ session: fakeSession,
443
+ cacheService: new InMemoryCacheService(),
444
+ propertiesService: new InMemoryPropertiesService(),
445
+ },
446
+
447
+ session: {
448
+ storageType: "cache",
449
+ },
450
+ });
451
+ ```
452
+
453
+ ## Scope
454
+
455
+ 現在の auth-core が提供するもの:
456
+
457
+ - User / Account / Session
458
+ - ProviderIdentity
459
+ - Email / Password authentication
460
+ - Apps Script Active User authentication
461
+ - Sign In
462
+ - Sign Up
463
+ - Sign Out
464
+ - Session management
465
+ - CacheService SessionStorage
466
+ - PropertiesService SessionStorage
467
+ - GAS runtime dependency injection
468
+
469
+ 以下は auth-core の責務外として後続 package / Issue で扱います。
470
+
471
+ - Schema / modelName / field mapping
472
+ - database session storage
473
+ - plugin system
474
+ - admin plugin
475
+ - Google OAuth
476
+ - organization / multi-tenant
477
+ - permission model
478
+ - MCP OAuth
479
+ - frontend UI
480
+ - `@gasboost/app` middleware integration
481
+
482
+ ## License
483
+
484
+ MIT
@@ -0,0 +1,51 @@
1
+ import { AppsScriptAuthPassword } from "./api/AppsScriptAuthPassword";
2
+ import { AppsScriptAuthSession } from "./api/AppsScriptAuthSession";
3
+ import { AppsScriptAuthSignIn } from "./api/AppsScriptAuthSignIn";
4
+ import { AppsScriptAuthSignOut } from "./api/AppsScriptAuthSignOut";
5
+ import { AppsScriptAuthSignUp } from "./api/AppsScriptAuthSignUp";
6
+ import { type AppsScriptAuthenticationOptions } from "./authentication/AppsScriptAuthentication";
7
+ import { type EmailPasswordAuthOptions } from "./authentication/EmailPasswordAuthentication";
8
+ import type { SessionStorageType } from "./factory/sessionStorageFactory";
9
+ import { AppsScriptAuthRepository } from "./storage/AppsScriptAuthRepository";
10
+ type SessionConfig = {
11
+ storageType: SessionStorageType;
12
+ expiresIn?: number;
13
+ };
14
+ type AppsScriptRuntime = {
15
+ utilities: GoogleAppsScript.Utilities.Utilities;
16
+ session: GoogleAppsScript.Base.Session;
17
+ cacheService: GoogleAppsScript.Cache.CacheService;
18
+ propertiesService: GoogleAppsScript.Properties.PropertiesService;
19
+ };
20
+ type AppsScriptAuthConfig<TEmailPassword extends EmailPasswordAuthOptions | undefined = undefined> = {
21
+ repository: AppsScriptAuthRepository;
22
+ session: SessionConfig;
23
+ runtime: AppsScriptRuntime;
24
+ emailPassword?: TEmailPassword;
25
+ appsScript?: AppsScriptAuthenticationOptions;
26
+ };
27
+ type BaseHandlers = {
28
+ signInEmail: AppsScriptAuthSignIn["email"];
29
+ signInAppsScript: AppsScriptAuthSignIn["appsScript"];
30
+ signUpEmail: AppsScriptAuthSignUp["email"];
31
+ signUpAppsScript: AppsScriptAuthSignUp["appsScript"];
32
+ getSession: AppsScriptAuthSession["get"];
33
+ signOut: AppsScriptAuthSignOut["execute"];
34
+ };
35
+ type PasswordHandlers = {
36
+ forgotPassword: AppsScriptAuthPassword["forgot"];
37
+ resetPassword: AppsScriptAuthPassword["reset"];
38
+ };
39
+ type AppsScriptAuthHandlers<TEmailPassword> = TEmailPassword extends {
40
+ passwordReset: unknown;
41
+ } ? BaseHandlers & PasswordHandlers : BaseHandlers;
42
+ export declare class AppsScriptAuth<TEmailPassword extends EmailPasswordAuthOptions | undefined = undefined> {
43
+ readonly signIn: AppsScriptAuthSignIn;
44
+ readonly signUp: AppsScriptAuthSignUp;
45
+ readonly session: AppsScriptAuthSession;
46
+ readonly signOut: AppsScriptAuthSignOut;
47
+ readonly password: AppsScriptAuthPassword | undefined;
48
+ constructor({ repository, session, runtime, emailPassword, appsScript, }: AppsScriptAuthConfig<TEmailPassword>);
49
+ get handlers(): AppsScriptAuthHandlers<TEmailPassword>;
50
+ }
51
+ export {};
@@ -0,0 +1,77 @@
1
+ import { AppsScriptAuthPassword } from "./api/AppsScriptAuthPassword";
2
+ import { AppsScriptAuthSession } from "./api/AppsScriptAuthSession";
3
+ import { AppsScriptAuthSignIn } from "./api/AppsScriptAuthSignIn";
4
+ import { AppsScriptAuthSignOut } from "./api/AppsScriptAuthSignOut";
5
+ import { AppsScriptAuthSignUp } from "./api/AppsScriptAuthSignUp";
6
+ import { AppsScriptAuthenticationConfig, } from "./authentication/AppsScriptAuthentication";
7
+ import { EmailPasswordAuthConfig, } from "./authentication/EmailPasswordAuthentication";
8
+ import { sessionStorageFactory } from "./factory/sessionStorageFactory";
9
+ export class AppsScriptAuth {
10
+ signIn;
11
+ signUp;
12
+ session;
13
+ signOut;
14
+ password;
15
+ constructor({ repository, session, runtime, emailPassword, appsScript, }) {
16
+ const sessionStorage = sessionStorageFactory(session.storageType, {
17
+ cacheService: runtime.cacheService,
18
+ propertiesService: runtime.propertiesService,
19
+ });
20
+ const expiresIn = session.expiresIn ?? 60 * 20;
21
+ const emailPasswordConfig = emailPassword
22
+ ? new EmailPasswordAuthConfig(emailPassword)
23
+ : undefined;
24
+ const appsScriptConfig = appsScript
25
+ ? new AppsScriptAuthenticationConfig(appsScript)
26
+ : undefined;
27
+ this.signIn = new AppsScriptAuthSignIn({
28
+ sessionStorage,
29
+ repository,
30
+ emailPassword: emailPasswordConfig,
31
+ appsScript: appsScriptConfig,
32
+ utilities: runtime.utilities,
33
+ session: runtime.session,
34
+ expiresIn,
35
+ });
36
+ this.signUp = new AppsScriptAuthSignUp({
37
+ sessionStorage,
38
+ expiresIn,
39
+ repository,
40
+ emailPassword: emailPasswordConfig,
41
+ appsScript: appsScriptConfig,
42
+ utilities: runtime.utilities,
43
+ session: runtime.session,
44
+ });
45
+ this.session = new AppsScriptAuthSession({
46
+ sessionStorage,
47
+ });
48
+ this.signOut = new AppsScriptAuthSignOut(sessionStorage);
49
+ if (emailPasswordConfig?.passwordReset) {
50
+ this.password = new AppsScriptAuthPassword({
51
+ repository,
52
+ utilities: runtime.utilities,
53
+ emailPassword: emailPasswordConfig,
54
+ });
55
+ }
56
+ else {
57
+ this.password = undefined;
58
+ }
59
+ }
60
+ get handlers() {
61
+ const handlers = {
62
+ signInEmail: this.signIn.email,
63
+ signInAppsScript: this.signIn.appsScript,
64
+ signUpEmail: this.signUp.email,
65
+ signUpAppsScript: this.signUp.appsScript,
66
+ getSession: this.session.get.bind(this.session),
67
+ signOut: this.signOut.execute.bind(this.signOut),
68
+ ...(this.password
69
+ ? {
70
+ forgotPassword: this.password.forgot.bind(this.password),
71
+ resetPassword: this.password.reset.bind(this.password),
72
+ }
73
+ : {}),
74
+ };
75
+ return handlers;
76
+ }
77
+ }
@@ -0,0 +1,5 @@
1
+ export declare const authPattern: {
2
+ readonly emailPassword: "emailPassword";
3
+ readonly appsScript: "appsScript";
4
+ };
5
+ export type AuthPattern = (typeof authPattern)[keyof typeof authPattern];
@@ -0,0 +1,4 @@
1
+ export const authPattern = {
2
+ emailPassword: "emailPassword",
3
+ appsScript: "appsScript",
4
+ };
@@ -0,0 +1,30 @@
1
+ import type { EmailPasswordAuthConfig } from "../authentication/EmailPasswordAuthentication";
2
+ import type { AppsScriptAuthRepository } from "../storage/AppsScriptAuthRepository";
3
+ export interface PasswordResetDelivery {
4
+ send({ email, token, expiresAt, }: {
5
+ email: string;
6
+ token: string;
7
+ expiresAt: Date;
8
+ }): Promise<void>;
9
+ }
10
+ export declare class AppsScriptAuthPassword {
11
+ readonly repository: AppsScriptAuthRepository;
12
+ readonly utilities: GoogleAppsScript.Utilities.Utilities;
13
+ readonly emailPassword: EmailPasswordAuthConfig;
14
+ readonly config: {
15
+ expiresIn: number;
16
+ delivery: PasswordResetDelivery;
17
+ };
18
+ constructor({ repository, utilities, emailPassword, }: {
19
+ repository: AppsScriptAuthRepository;
20
+ utilities: GoogleAppsScript.Utilities.Utilities;
21
+ emailPassword: EmailPasswordAuthConfig;
22
+ });
23
+ forgot({ email }: {
24
+ email: string;
25
+ }): Promise<void>;
26
+ reset({ token, newPassword, }: {
27
+ token: string;
28
+ newPassword: string;
29
+ }): Promise<void>;
30
+ }
@@ -0,0 +1,64 @@
1
+ import { authPattern } from "../AuthPattern";
2
+ import { InvalidPasswordResetError } from "../domain/Errors";
3
+ import { Password } from "../domain/Password";
4
+ import { PasswordCredential } from "../domain/PasswordCredential";
5
+ import { PasswordResetToken } from "../domain/PasswordResetToken";
6
+ import { EmailPasswordIdentity } from "../identity/EmailPasswordIdentity";
7
+ export class AppsScriptAuthPassword {
8
+ repository;
9
+ utilities;
10
+ emailPassword;
11
+ config;
12
+ constructor({ repository, utilities, emailPassword, }) {
13
+ emailPassword.ensurePasswordResetEnabled();
14
+ this.repository = repository;
15
+ this.utilities = utilities;
16
+ this.emailPassword = emailPassword;
17
+ const passwordReset = emailPassword.passwordReset;
18
+ if (!passwordReset) {
19
+ throw new Error("Password reset is not enabled");
20
+ }
21
+ this.config = {
22
+ expiresIn: passwordReset.expiresIn ?? 60 * 60,
23
+ delivery: passwordReset.delivery,
24
+ };
25
+ }
26
+ async forgot({ email }) {
27
+ const account = await this.repository.account.findByIdentity(authPattern.emailPassword, email);
28
+ if (account === null) {
29
+ return;
30
+ }
31
+ if (!(account.identity instanceof EmailPasswordIdentity)) {
32
+ throw new Error("Invalid account identity");
33
+ }
34
+ const now = new Date();
35
+ const expiresAt = new Date(now.getTime() + this.config.expiresIn * 1000);
36
+ const { credential, token } = PasswordCredential.generate({
37
+ account,
38
+ utilities: this.utilities,
39
+ expiresAt,
40
+ });
41
+ await this.repository.passwordCredential.save(credential);
42
+ await this.config.delivery.send({
43
+ email,
44
+ token,
45
+ expiresAt,
46
+ });
47
+ }
48
+ async reset({ token, newPassword, }) {
49
+ const passwordResetToken = new PasswordResetToken(token);
50
+ const credential = await this.repository.passwordCredential.findByResetTokenHash(passwordResetToken.hash(this.utilities));
51
+ if (credential === null) {
52
+ throw new InvalidPasswordResetError();
53
+ }
54
+ const password = new Password(newPassword, this.utilities, this.emailPassword.pepper, this.emailPassword.iterations);
55
+ const hashedPassword = await password.hash(credential.account.id);
56
+ const updatedCredential = credential.resetPassword({
57
+ token,
58
+ newPassword: hashedPassword,
59
+ utilities: this.utilities,
60
+ now: new Date(),
61
+ });
62
+ await this.repository.passwordCredential.save(updatedCredential);
63
+ }
64
+ }