@gasboost/auth 0.7.0 → 0.8.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/README.md CHANGED
@@ -691,3 +691,33 @@ const auth = new AppsScriptAuth({
691
691
  ## License
692
692
 
693
693
  MIT
694
+
695
+ ### 再実行可能な複数認証方式の登録(0.8.0)
696
+
697
+ `registration.methods` は設定の `enabled` を返します。
698
+ `registration.ensure({ id, name, email, password? })` は、その全認証方式を
699
+ 同じ User ID に登録します。通常の signup 設定とは独立した、サーバー側の管理用 API です。
700
+ 呼び出し元で権限を検証し、Script Lock などで登録全体を排他制御してください。
701
+ Apps Script 認証が有効なら `email` は `Session.getActiveUser().getEmail()` と一致する必要があります。
702
+
703
+ ```ts
704
+ const user = await auth.registration.ensure({
705
+ id: recovery.userId, // 初回書き込み前に永続化した固定 ID
706
+ name: "システム管理者",
707
+ email: Session.getActiveUser().getEmail(),
708
+ password: Utilities.getUuid() + Utilities.getUuid(),
709
+ });
710
+ // Email が有効な場合は password reset delivery で本人に設定を促す。
711
+ await auth.password?.forgot({ email: Session.getActiveUser().getEmail() });
712
+ ```
713
+
714
+ 既存の認証情報は維持し、不足する Account だけを固定 ID で追加します。
715
+ 別 User が所有するメールアドレスは取り込みません。仮パスワードを本人へ送信しないでください。
716
+ メール通知失敗時は `password.forgot()` を再実行して新しいリセットトークンを送れます。
717
+ 再発行すると以前のリセットトークンは失効します。
718
+
719
+ この API は Repository の任意メソッド `user.save(user)` を必要とします。
720
+ 実装は User と各 Account を ID で upsert し、途中失敗後も同じ ID で修復できる必要があります。
721
+ `@gasboost/auth-sheetorm >=0.4.0` はこれを実装します。
722
+ 既存の `registration.email()` / `.appsScript()` と `user.create()` の動作は変更しません。
723
+ 関連: [gasboost/app#64](https://github.com/gasboost/app/issues/64)。
@@ -1,12 +1,20 @@
1
1
  import type { AppsScriptAuthenticationConfig } from "../authentication/AppsScriptAuthentication";
2
2
  import type { EmailPasswordAuthConfig } from "../authentication/EmailPasswordAuthentication";
3
- import type { User } from "../domain/User";
3
+ import { User } from "../domain/User";
4
4
  import { type AppsScriptRegistrationInput } from "../registration/AppsScriptRegistration";
5
5
  import { type EmailPasswordRegistrationInput } from "../registration/EmailPasswordRegistration";
6
6
  import type { AppsScriptAuthRepository } from "../storage/AppsScriptAuthRepository";
7
7
  export declare class AppsScriptAuthRegistration {
8
8
  private readonly emailPasswordRegistration?;
9
9
  private readonly appsScriptRegistration?;
10
+ readonly methods: {
11
+ readonly emailPassword: boolean;
12
+ readonly appsScript: boolean;
13
+ };
14
+ private readonly repository;
15
+ private readonly utilities;
16
+ private readonly session;
17
+ private readonly emailPassword?;
10
18
  constructor({ emailPassword, appsScript, repository, utilities, session, }: {
11
19
  emailPassword?: EmailPasswordAuthConfig;
12
20
  appsScript?: AppsScriptAuthenticationConfig;
@@ -16,4 +24,11 @@ export declare class AppsScriptAuthRegistration {
16
24
  });
17
25
  email(input: EmailPasswordRegistrationInput): Promise<User>;
18
26
  appsScript(input: AppsScriptRegistrationInput): Promise<User>;
27
+ /** Privileged server API. Caller must authorize and serialize provisioning. */
28
+ ensure(input: {
29
+ id: string;
30
+ name: string;
31
+ email: string;
32
+ password?: string;
33
+ }): Promise<User>;
19
34
  }
@@ -1,12 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AppsScriptAuthRegistration = void 0;
4
+ const User_1 = require("../domain/User");
5
+ const Account_1 = require("../domain/Account");
6
+ const Password_1 = require("../domain/Password");
7
+ const AuthPattern_1 = require("../AuthPattern");
8
+ const AppsScriptIdentity_1 = require("../identity/AppsScriptIdentity");
9
+ const EmailPasswordIdentity_1 = require("../identity/EmailPasswordIdentity");
4
10
  const AppsScriptRegistration_1 = require("../registration/AppsScriptRegistration");
5
11
  const EmailPasswordRegistration_1 = require("../registration/EmailPasswordRegistration");
6
12
  class AppsScriptAuthRegistration {
7
13
  emailPasswordRegistration;
8
14
  appsScriptRegistration;
15
+ methods;
16
+ repository;
17
+ utilities;
18
+ session;
19
+ emailPassword;
9
20
  constructor({ emailPassword, appsScript, repository, utilities, session, }) {
21
+ this.repository = repository;
22
+ this.utilities = utilities;
23
+ this.session = session;
24
+ this.emailPassword = emailPassword;
25
+ this.methods = Object.freeze({
26
+ emailPassword: !!emailPassword?.enabled,
27
+ appsScript: !!appsScript?.enabled,
28
+ });
10
29
  if (emailPassword?.enabled) {
11
30
  this.emailPasswordRegistration = new EmailPasswordRegistration_1.EmailPasswordRegistration(repository, utilities, emailPassword);
12
31
  }
@@ -26,5 +45,67 @@ class AppsScriptAuthRegistration {
26
45
  }
27
46
  return this.appsScriptRegistration.register(input);
28
47
  }
48
+ /** Privileged server API. Caller must authorize and serialize provisioning. */
49
+ async ensure(input) {
50
+ if (!this.methods.emailPassword && !this.methods.appsScript) {
51
+ throw new Error("No authentication method is enabled");
52
+ }
53
+ if (!input.id || !input.email.trim())
54
+ throw new Error("User ID and email are required");
55
+ if (!this.repository.user.save)
56
+ throw new Error("Repository must support idempotent user.save");
57
+ if (this.methods.appsScript &&
58
+ this.session.getActiveUser().getEmail() !== input.email) {
59
+ throw new Error("Active user does not match registration email");
60
+ }
61
+ const existing = await this.repository.user.find(input.id);
62
+ const accounts = [...(existing?.accounts ?? [])];
63
+ for (const provider of [
64
+ AuthPattern_1.authPattern.emailPassword,
65
+ AuthPattern_1.authPattern.appsScript,
66
+ ]) {
67
+ if (provider === AuthPattern_1.authPattern.emailPassword
68
+ ? !this.methods.emailPassword
69
+ : !this.methods.appsScript)
70
+ continue;
71
+ const collision = await this.repository.account.findByIdentity(provider, input.email);
72
+ if (collision && collision.userId !== input.id)
73
+ throw new Error("Account belongs to another user");
74
+ const current = accounts.find((account) => account.identity.providerName === provider);
75
+ if (current) {
76
+ if (current.userId !== input.id ||
77
+ current.identity.accountId !== input.email)
78
+ throw new Error("Existing user identity does not match");
79
+ continue;
80
+ }
81
+ if (collision) {
82
+ accounts.push(collision);
83
+ continue;
84
+ }
85
+ const accountId = `${input.id}:${provider}`;
86
+ let identity;
87
+ if (provider === AuthPattern_1.authPattern.emailPassword) {
88
+ if (!input.password || !this.emailPassword)
89
+ throw new Error("Password is required");
90
+ const password = new Password_1.Password(input.password, this.utilities, this.emailPassword.pepper, this.emailPassword.iterations);
91
+ identity = new EmailPasswordIdentity_1.EmailPasswordIdentity({
92
+ accountId: input.email,
93
+ password: await password.hash(accountId),
94
+ });
95
+ }
96
+ else {
97
+ identity = new AppsScriptIdentity_1.AppsScriptIdentity({
98
+ googleAccountAddress: input.email,
99
+ });
100
+ }
101
+ accounts.push(new Account_1.Account({ id: accountId, userId: input.id, identity }));
102
+ }
103
+ const user = new User_1.User({
104
+ id: input.id,
105
+ name: existing?.name ?? input.name,
106
+ accounts,
107
+ });
108
+ return this.repository.user.save(user);
109
+ }
29
110
  }
30
111
  exports.AppsScriptAuthRegistration = AppsScriptAuthRegistration;
@@ -9,6 +9,8 @@ export interface AppsScriptAuthRepository {
9
9
  user: {
10
10
  find: (id: string) => Promise<User | null>;
11
11
  create: (user: User) => Promise<User>;
12
+ /** Upsert user/accounts by their stable IDs; retries must preserve a single user. */
13
+ save?: (user: User) => Promise<User>;
12
14
  };
13
15
  passwordCredential: {
14
16
  findByResetTokenHash(tokenHash: string): Promise<PasswordCredential | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gasboost/auth",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Modern authentication for Google Apps Script with email/password, Apps Script authentication, sessions, and password reset.",
5
5
  "keywords": [
6
6
  "google-apps-script",