@gasboost/auth 0.8.0 → 0.9.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
@@ -281,12 +281,13 @@ const user = await auth.registration.email({
281
281
  ```ts
282
282
  const user = await auth.registration.appsScript({
283
283
  name: "Taro",
284
+ email: "taro@example.com",
284
285
  });
285
286
  ```
286
287
 
287
- Apps Script方式では
288
- `Session.getActiveUser().getEmail()` を使用するため、
289
- 任意の他ユーザーを指定して登録することはできません。
288
+ Registration は指定された対象者の新規 User と最初の Account を作成します。
289
+ Session は参照しません。同じ provider / email の重複は拒否し、
290
+ 別方式の User とメールが一致しても自動統合しません。
290
291
 
291
292
  ### Sign Upとの違い
292
293
 
@@ -692,32 +693,55 @@ const auth = new AppsScriptAuth({
692
693
 
693
694
  MIT
694
695
 
695
- ### 再実行可能な複数認証方式の登録(0.8.0)
696
+ ## 既存 User への Account 追加(0.9.0)
696
697
 
697
698
  `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()` と一致する必要があります。
699
+ `auth.account.add(userId, input)` は明示した既存 User に Account を追加して返します。
700
+ 新規 User や Session は作成せず、`isSignupEnabled` に依存しません。
701
+ 同一 Identity の重複、他 User の Identity、同じ User の同一方式の追加は拒否します。
702
+ 対象の認証方式は `enabled: true` が必要です。
702
703
 
703
704
  ```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() });
705
+ // 呼び出し元で初期化権限・実行主体を検証した後に実行
706
+ const email = Session.getEffectiveUser().getEmail();
707
+ const user = auth.registration.methods.appsScript
708
+ ? await auth.registration.appsScript({ name: "システム管理者", email })
709
+ : await auth.registration.email({
710
+ name: "システム管理者",
711
+ email,
712
+ password: temporaryPassword,
713
+ });
714
+
715
+ if (
716
+ auth.registration.methods.appsScript &&
717
+ auth.registration.methods.emailPassword
718
+ ) {
719
+ await auth.account.add(user.id, {
720
+ provider: "emailPassword",
721
+ email,
722
+ password: temporaryPassword,
723
+ });
724
+ }
725
+ // Apps Script Account を既存 User に追加する場合:
726
+ // await auth.account.add(userId, { provider: "appsScript", email });
712
727
  ```
713
728
 
714
- 既存の認証情報は維持し、不足する Account だけを固定 ID で追加します。
715
- 別 User が所有するメールアドレスは取り込みません。仮パスワードを本人へ送信しないでください。
716
- メール通知失敗時は `password.forgot()` を再実行して新しいリセットトークンを送れます。
717
- 再発行すると以前のリセットトークンは失効します。
729
+ メールとパスワードの Account は新しい Account ID を salt としてパスワードをハッシュ化します。
730
+ 既存 Account や User 名は変更しません。
731
+ 呼び出し元で対象 User への追加権限を検証してください。
732
+ `@gasboost/auth-app` の `handlers(auth)` はこの管理用 API を RPC に公開しません。
733
+ 公開 `signUp.appsScript({ name })` は引き続き ActiveUser を登録し、Session を発行します。
734
+
735
+ ### Repository と移行
736
+
737
+ 0.9.0 では `registration.appsScript({ name })` を `{ name, email }` に変更しました。
738
+ 0.8.0 の `registration.ensure()` と任意の `repository.user.save()` は削除しました。
739
+ 旧 PR の EffectiveUser 専用登録経路も提供しません。
740
+ 通常の `registration.email()` と公開 SignUp の契約は維持します。
718
741
 
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)。
742
+ Account 追加には任意の Repository メソッド `account.create(account): Promise<Account>` が必要です。
743
+ 未対応の Repository は既存 API を継続利用でき、`account.add` 呼び出し時にエラーになります。
744
+ 実装は存在する User への追加だけを許可し、Account ID・Identity・User ごとの同一方式の重複を
745
+ 保存と同じ transaction 内で再検証してください。`@gasboost/auth-sheetorm >=0.4.1` が対応します。
746
+ 複数 API にまたがる初期化の排他制御、途中失敗時の保存済み User ID の復旧や
747
+ 再開処理は呼び出し元(Bootstrap 等)の責務です。
@@ -1,3 +1,4 @@
1
+ import { AppsScriptAuthAccount } from "./api/AppsScriptAuthAccount";
1
2
  import { AppsScriptAuthPassword } from "./api/AppsScriptAuthPassword";
2
3
  import { AppsScriptAuthRegistration } from "./api/AppsScriptAuthRegistration";
3
4
  import { AppsScriptAuthSession } from "./api/AppsScriptAuthSession";
@@ -37,6 +38,7 @@ export declare class AppsScriptAuth<TEmailPassword extends EmailPasswordAuthOpti
37
38
  readonly signOut: AppsScriptAuthSignOut;
38
39
  readonly password: AppsScriptAuthPasswordApi<TEmailPassword>;
39
40
  readonly registration: AppsScriptAuthRegistration;
41
+ readonly account: AppsScriptAuthAccount;
40
42
  constructor({ repository, session, runtime, emailPassword, appsScript, hooks, }: AppsScriptAuthConfig<TEmailPassword, THookResult>);
41
43
  }
42
44
  export {};
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AppsScriptAuth = void 0;
4
+ const AppsScriptAuthAccount_1 = require("./api/AppsScriptAuthAccount");
4
5
  const AppsScriptAuthPassword_1 = require("./api/AppsScriptAuthPassword");
5
6
  const AppsScriptAuthRegistration_1 = require("./api/AppsScriptAuthRegistration");
6
7
  const AppsScriptAuthSession_1 = require("./api/AppsScriptAuthSession");
@@ -17,6 +18,7 @@ class AppsScriptAuth {
17
18
  signOut;
18
19
  password;
19
20
  registration;
21
+ account;
20
22
  constructor({ repository, session, runtime, emailPassword, appsScript, hooks, }) {
21
23
  const sessionStorage = (0, sessionStorageFactory_1.sessionStorageFactory)(session.storageType, {
22
24
  cacheService: runtime.cacheService,
@@ -64,12 +66,17 @@ class AppsScriptAuth {
64
66
  emailPassword: emailPasswordConfig,
65
67
  })
66
68
  : undefined);
69
+ this.account = new AppsScriptAuthAccount_1.AppsScriptAuthAccount({
70
+ emailPassword: emailPasswordConfig,
71
+ appsScript: appsScriptConfig,
72
+ repository,
73
+ utilities: runtime.utilities,
74
+ });
67
75
  this.registration = new AppsScriptAuthRegistration_1.AppsScriptAuthRegistration({
68
76
  emailPassword: emailPasswordConfig,
69
77
  appsScript: appsScriptConfig,
70
78
  repository,
71
79
  utilities: runtime.utilities,
72
- session: runtime.session,
73
80
  });
74
81
  }
75
82
  }
@@ -0,0 +1,23 @@
1
+ import type { AppsScriptAuthenticationConfig } from "../authentication/AppsScriptAuthentication";
2
+ import type { EmailPasswordAuthConfig } from "../authentication/EmailPasswordAuthentication";
3
+ import { Account } from "../domain/Account";
4
+ import type { AppsScriptAuthRepository } from "../storage/AppsScriptAuthRepository";
5
+ export type AddAccountInput = {
6
+ provider: "appsScript";
7
+ email: string;
8
+ } | {
9
+ provider: "emailPassword";
10
+ email: string;
11
+ password: string;
12
+ };
13
+ /** Server API. The caller must authorize account linking to the specified user. */
14
+ export declare class AppsScriptAuthAccount {
15
+ private readonly config;
16
+ constructor(config: {
17
+ repository: AppsScriptAuthRepository;
18
+ utilities: GoogleAppsScript.Utilities.Utilities;
19
+ emailPassword?: EmailPasswordAuthConfig;
20
+ appsScript?: AppsScriptAuthenticationConfig;
21
+ });
22
+ add(userId: string, input: AddAccountInput): Promise<Account>;
23
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppsScriptAuthAccount = void 0;
4
+ const AuthPattern_1 = require("../AuthPattern");
5
+ const Account_1 = require("../domain/Account");
6
+ const Password_1 = require("../domain/Password");
7
+ const AppsScriptIdentity_1 = require("../identity/AppsScriptIdentity");
8
+ const EmailPasswordIdentity_1 = require("../identity/EmailPasswordIdentity");
9
+ /** Server API. The caller must authorize account linking to the specified user. */
10
+ class AppsScriptAuthAccount {
11
+ config;
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ async add(userId, input) {
16
+ const { repository, utilities, emailPassword, appsScript } = this.config;
17
+ if (!userId?.trim() || !input.email?.trim())
18
+ throw new Error("User ID and email are required");
19
+ switch (input.provider) {
20
+ case AuthPattern_1.authPattern.emailPassword:
21
+ if (!emailPassword?.enabled)
22
+ throw new Error("Email and password authentication is disabled");
23
+ break;
24
+ case AuthPattern_1.authPattern.appsScript:
25
+ if (!appsScript?.enabled)
26
+ throw new Error("Apps Script authentication is disabled");
27
+ break;
28
+ default:
29
+ throw new Error("Unsupported auth provider");
30
+ }
31
+ if (!repository.account.create)
32
+ throw new Error("Repository must support account.create");
33
+ const user = await repository.user.find(userId);
34
+ if (!user || user.id !== userId)
35
+ throw new Error("User not found");
36
+ if (user.account(input.provider))
37
+ throw new Error("User already has an account for this provider");
38
+ if (await repository.account.findByIdentity(input.provider, input.email))
39
+ throw new Error("Account already registered");
40
+ const id = utilities.getUuid();
41
+ const identity = input.provider === AuthPattern_1.authPattern.emailPassword
42
+ ? new EmailPasswordIdentity_1.EmailPasswordIdentity({
43
+ accountId: input.email,
44
+ password: await new Password_1.Password(input.password, utilities, emailPassword.pepper, emailPassword.iterations).hash(id),
45
+ })
46
+ : new AppsScriptIdentity_1.AppsScriptIdentity({ googleAccountAddress: input.email });
47
+ return repository.account.create(new Account_1.Account({ id, userId, identity }));
48
+ }
49
+ }
50
+ exports.AppsScriptAuthAccount = AppsScriptAuthAccount;
@@ -11,24 +11,12 @@ export declare class AppsScriptAuthRegistration {
11
11
  readonly emailPassword: boolean;
12
12
  readonly appsScript: boolean;
13
13
  };
14
- private readonly repository;
15
- private readonly utilities;
16
- private readonly session;
17
- private readonly emailPassword?;
18
- constructor({ emailPassword, appsScript, repository, utilities, session, }: {
14
+ constructor({ emailPassword, appsScript, repository, utilities, }: {
19
15
  emailPassword?: EmailPasswordAuthConfig;
20
16
  appsScript?: AppsScriptAuthenticationConfig;
21
17
  repository: AppsScriptAuthRepository;
22
18
  utilities: GoogleAppsScript.Utilities.Utilities;
23
- session: GoogleAppsScript.Base.Session;
24
19
  });
25
20
  email(input: EmailPasswordRegistrationInput): Promise<User>;
26
21
  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>;
34
22
  }
@@ -1,27 +1,13 @@
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");
10
4
  const AppsScriptRegistration_1 = require("../registration/AppsScriptRegistration");
11
5
  const EmailPasswordRegistration_1 = require("../registration/EmailPasswordRegistration");
12
6
  class AppsScriptAuthRegistration {
13
7
  emailPasswordRegistration;
14
8
  appsScriptRegistration;
15
9
  methods;
16
- repository;
17
- utilities;
18
- session;
19
- emailPassword;
20
- constructor({ emailPassword, appsScript, repository, utilities, session, }) {
21
- this.repository = repository;
22
- this.utilities = utilities;
23
- this.session = session;
24
- this.emailPassword = emailPassword;
10
+ constructor({ emailPassword, appsScript, repository, utilities, }) {
25
11
  this.methods = Object.freeze({
26
12
  emailPassword: !!emailPassword?.enabled,
27
13
  appsScript: !!appsScript?.enabled,
@@ -30,7 +16,7 @@ class AppsScriptAuthRegistration {
30
16
  this.emailPasswordRegistration = new EmailPasswordRegistration_1.EmailPasswordRegistration(repository, utilities, emailPassword);
31
17
  }
32
18
  if (appsScript?.enabled) {
33
- this.appsScriptRegistration = new AppsScriptRegistration_1.AppsScriptRegistration(utilities, session, repository);
19
+ this.appsScriptRegistration = new AppsScriptRegistration_1.AppsScriptRegistration(utilities, repository);
34
20
  }
35
21
  }
36
22
  async email(input) {
@@ -45,67 +31,5 @@ class AppsScriptAuthRegistration {
45
31
  }
46
32
  return this.appsScriptRegistration.register(input);
47
33
  }
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
- }
110
34
  }
111
35
  exports.AppsScriptAuthRegistration = AppsScriptAuthRegistration;
@@ -8,7 +8,7 @@ import type { AppsScriptSessionStorage } from "../storage/AppsScriptSessionStora
8
8
  import { SignUp } from "./SignUp";
9
9
  export declare class AppsScriptAuthSignUp<THookResult = undefined> {
10
10
  email: SignUp<EmailPasswordRegistrationInput, THookResult>["execute"];
11
- appsScript: SignUp<AppsScriptRegistrationInput, THookResult>["execute"];
11
+ appsScript: SignUp<Pick<AppsScriptRegistrationInput, "name">, THookResult>["execute"];
12
12
  constructor({ sessionStorage, expiresIn, afterSignIn, repository, emailPassword, appsScript, utilities, session, }: {
13
13
  sessionStorage: AppsScriptSessionStorage;
14
14
  expiresIn: number;
@@ -27,14 +27,17 @@ class AppsScriptAuthSignUp {
27
27
  throw new Error("Apps Script authentication is disabled");
28
28
  }
29
29
  appsScript.ensureSignUpEnabled();
30
+ const email = session.getActiveUser().getEmail();
31
+ if (!email)
32
+ throw new Error("No active user found");
30
33
  const appsScriptSignUp = new SignUp_1.SignUp({
31
34
  sessionStorage,
32
35
  afterSignIn,
33
36
  expiresIn,
34
- registration: new AppsScriptRegistration_1.AppsScriptRegistration(utilities, session, repository),
37
+ registration: new AppsScriptRegistration_1.AppsScriptRegistration(utilities, repository),
35
38
  utilities,
36
39
  });
37
- return appsScriptSignUp.execute(input);
40
+ return appsScriptSignUp.execute({ name: input.name, email });
38
41
  };
39
42
  }
40
43
  }
package/dist/index.d.ts CHANGED
@@ -16,3 +16,4 @@ export { PasswordReset } from "./domain/PasswordReset";
16
16
  export { AppsScriptIdentity } from "./identity/AppsScriptIdentity";
17
17
  export { EmailPasswordIdentity } from "./identity/EmailPasswordIdentity";
18
18
  export type { AfterSignInContext, AfterSignInHook, AuthHooks, } from "./hooks/AuthHooks";
19
+ export type { AddAccountInput } from "./api/AppsScriptAuthAccount";
@@ -3,11 +3,11 @@ import type { AppsScriptAuthRepository } from "../storage/AppsScriptAuthReposito
3
3
  import type { Registration } from "./Registration";
4
4
  export interface AppsScriptRegistrationInput {
5
5
  readonly name: string;
6
+ readonly email: string;
6
7
  }
7
8
  export declare class AppsScriptRegistration implements Registration<AppsScriptRegistrationInput> {
8
9
  private readonly utilities;
9
- private readonly session;
10
10
  private readonly repository;
11
- constructor(utilities: GoogleAppsScript.Utilities.Utilities, session: GoogleAppsScript.Base.Session, repository: AppsScriptAuthRepository);
11
+ constructor(utilities: GoogleAppsScript.Utilities.Utilities, repository: AppsScriptAuthRepository);
12
12
  register(input: AppsScriptRegistrationInput): Promise<User>;
13
13
  }
@@ -7,19 +7,15 @@ const User_1 = require("../domain/User");
7
7
  const AppsScriptIdentity_1 = require("../identity/AppsScriptIdentity");
8
8
  class AppsScriptRegistration {
9
9
  utilities;
10
- session;
11
10
  repository;
12
- constructor(utilities, session, repository) {
11
+ constructor(utilities, repository) {
13
12
  this.utilities = utilities;
14
- this.session = session;
15
13
  this.repository = repository;
16
14
  }
17
15
  async register(input) {
18
- // Session.getActiveUser() から識別子取得
19
- const email = this.session.getActiveUser().getEmail();
20
- if (!email) {
21
- throw new Error("No active user found");
22
- }
16
+ const email = input.email;
17
+ if (!email?.trim())
18
+ throw new Error("Email is required");
23
19
  const existingAccount = await this.repository.account.findByIdentity(AuthPattern_1.authPattern.appsScript, email);
24
20
  if (existingAccount) {
25
21
  throw new Error("Account already registered");
@@ -4,13 +4,13 @@ import type { PasswordCredential } from "../domain/PasswordCredential";
4
4
  import type { User } from "../domain/User";
5
5
  export interface AppsScriptAuthRepository {
6
6
  account: {
7
+ /** Create an account for an existing user. Reject identity, ID and provider duplicates atomically. */
8
+ create?: (account: Account) => Promise<Account>;
7
9
  findByIdentity(provider: AuthPattern, identifier: string): Promise<Account | null>;
8
10
  };
9
11
  user: {
10
12
  find: (id: string) => Promise<User | null>;
11
13
  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>;
14
14
  };
15
15
  passwordCredential: {
16
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.8.0",
3
+ "version": "0.9.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",