@gasboost/auth-app 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.
package/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # @gasboost/auth-app
2
+
3
+ `@gasboost/auth` と `@gasboost/app` を接続する authentication middleware adapter です。
4
+
5
+ 認証必須 RPC の input に session token を含め、RPC 実行前に既存の `auth.session.get(token)` を利用して session を検証します。
6
+
7
+ 有効な session は `AppsScript` の state に保存されます。
8
+
9
+ ```text
10
+ RPC input
11
+
12
+ token
13
+
14
+ @gasboost/auth-app
15
+
16
+ auth.session.get(token)
17
+
18
+ Session
19
+
20
+ AppsScript state
21
+ ```
22
+
23
+ `@gasboost/app` 自体は `@gasboost/auth` を認識しません。
24
+
25
+ ```text
26
+ @gasboost/auth
27
+
28
+ @gasboost/auth-app → @gasboost/app
29
+ ```
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pnpm add @gasboost/auth-app @gasboost/auth @gasboost/app
35
+ ```
36
+
37
+ npm:
38
+
39
+ ```bash
40
+ npm install @gasboost/auth-app @gasboost/auth @gasboost/app
41
+ ```
42
+
43
+ ## AuthenticatedInput
44
+
45
+ 認証が必要な RPC では `AuthenticatedInput` を handler input に指定します。
46
+
47
+ ```ts
48
+ import type { AuthenticatedInput } from "@gasboost/auth-app";
49
+ ```
50
+
51
+ token だけ必要な場合:
52
+
53
+ ```ts
54
+ type Input = AuthenticatedInput;
55
+ ```
56
+
57
+ これは以下の型になります。
58
+
59
+ ```ts
60
+ type Input = {
61
+ token: string;
62
+ };
63
+ ```
64
+
65
+ 追加の input がある場合は generic parameter に object を指定します。
66
+
67
+ ```ts
68
+ type Input = AuthenticatedInput<{
69
+ name: string;
70
+ }>;
71
+ ```
72
+
73
+ これは以下の型になります。
74
+
75
+ ```ts
76
+ type Input = {
77
+ name: string;
78
+ token: string;
79
+ };
80
+ ```
81
+
82
+ ## Authentication middleware
83
+
84
+ 既存の `AppsScriptAuth` instance を `authentication()` に渡します。
85
+
86
+ session storage や repository を middleware 側で再定義する必要はありません。
87
+
88
+ ```ts
89
+ import { AppsScript } from "@gasboost/app";
90
+ import { AppsScriptAuth } from "@gasboost/auth";
91
+ import { authentication, type AuthState } from "@gasboost/auth-app";
92
+
93
+ const auth = new AppsScriptAuth({
94
+ repository,
95
+
96
+ runtime: {
97
+ utilities: Utilities,
98
+ session: Session,
99
+ cacheService: CacheService,
100
+ propertiesService: PropertiesService,
101
+ },
102
+
103
+ session: {
104
+ storageType: "cache",
105
+ },
106
+ });
107
+
108
+ const app = new AppsScript<AuthState>().use(authentication(auth));
109
+ ```
110
+
111
+ `authentication()` は `AppsScriptAuth` の既存 session API を利用します。
112
+
113
+ ```ts
114
+ const session = await auth.session.get(token);
115
+ ```
116
+
117
+ session の取得だけを目的とした別の authentication API は使用しません。
118
+
119
+ ## Authenticated RPC
120
+
121
+ 認証が必要な RPC は `AuthenticatedInput` を利用します。
122
+
123
+ ```ts
124
+ import { AppsScript } from "@gasboost/app";
125
+ import {
126
+ authentication,
127
+ type AuthenticatedInput,
128
+ type AuthState,
129
+ } from "@gasboost/auth-app";
130
+
131
+ const app = new AppsScript<AuthState>()
132
+ .use(authentication(auth))
133
+ .call("getProfile", (_input: AuthenticatedInput) => {
134
+ const session = app.state.get("session");
135
+
136
+ if (!session) {
137
+ throw new Error("Unauthorized");
138
+ }
139
+
140
+ return getProfile(session.userId);
141
+ });
142
+ ```
143
+
144
+ クライアント側では token が必須になります。
145
+
146
+ ```ts
147
+ client.getProfile({
148
+ token,
149
+ });
150
+ ```
151
+
152
+ token を渡さない呼び出しは RPC contract 上エラーになります。
153
+
154
+ 追加 input が必要な場合:
155
+
156
+ ```ts
157
+ const app = new AppsScript<AuthState>().use(authentication(auth)).call(
158
+ "updateProfile",
159
+ (
160
+ input: AuthenticatedInput<{
161
+ name: string;
162
+ }>,
163
+ ) => {
164
+ const session = app.state.get("session");
165
+
166
+ if (!session) {
167
+ throw new Error("Unauthorized");
168
+ }
169
+
170
+ return updateProfile({
171
+ userId: session.userId,
172
+ name: input.name,
173
+ });
174
+ },
175
+ );
176
+ ```
177
+
178
+ クライアント:
179
+
180
+ ```ts
181
+ client.updateProfile({
182
+ token,
183
+ name: "Taro",
184
+ });
185
+ ```
186
+
187
+ ## Public RPC
188
+
189
+ middleware はすべての RPC に認証を要求するわけではありません。
190
+
191
+ token を持たない RPC input はそのまま後続 handler へ流れます。
192
+
193
+ ```ts
194
+ const app = new AppsScript<AuthState>()
195
+ .use(authentication(auth))
196
+ .call("signIn", (input: { email: string; password: string }) => {
197
+ return auth.signIn.email(input);
198
+ });
199
+ ```
200
+
201
+ クライアント側でも token は不要です。
202
+
203
+ ```ts
204
+ client.signIn({
205
+ email: "user@example.com",
206
+ password: "password",
207
+ });
208
+ ```
209
+
210
+ input 自体を持たない公開 RPC も利用できます。
211
+
212
+ ```ts
213
+ const app = new AppsScript<AuthState>()
214
+ .use(authentication(auth))
215
+ .call("health", () => {
216
+ return {
217
+ ok: true,
218
+ };
219
+ });
220
+ ```
221
+
222
+ ## Session state
223
+
224
+ 認証に成功すると middleware は取得した session を `AppsScript` state に設定します。
225
+
226
+ ```ts
227
+ context.state.set("session", session);
228
+ ```
229
+
230
+ application handler からは次のように取得できます。
231
+
232
+ ```ts
233
+ const session = app.state.get("session");
234
+
235
+ if (!session) {
236
+ throw new Error("Unauthorized");
237
+ }
238
+
239
+ session.id;
240
+ session.userId;
241
+ session.createdAt;
242
+ session.expiresAt;
243
+ ```
244
+
245
+ application user が必要な場合は `session.userId` を利用して application 側で取得します。
246
+
247
+ ```ts
248
+ const session = app.state.get("session");
249
+
250
+ if (!session) {
251
+ throw new Error("Unauthorized");
252
+ }
253
+
254
+ const user = await userRepository.find(session.userId);
255
+ ```
256
+
257
+ `@gasboost/auth-app` は application User の取得までは担当しません。
258
+
259
+ ## Unauthorized
260
+
261
+ token プロパティが存在する場合、middleware は token を検証します。
262
+
263
+ 以下の場合は `Unauthorized` error になります。
264
+
265
+ - `token` が string ではない
266
+ - `auth.session.get(token)` が session を返さない
267
+ - session が期限切れ
268
+
269
+ ```ts
270
+ throw new Error("Unauthorized");
271
+ ```
272
+
273
+ 期限切れ session の判定と削除は `@gasboost/auth` の既存 `session.get()` が担当します。
274
+
275
+ middleware 側で session storage を直接操作することはありません。
276
+
277
+ ## Apps Script identity
278
+
279
+ `Session.getActiveUser().getEmail()` と application session は別の概念です。
280
+
281
+ ```text
282
+ Session.getActiveUser().getEmail()
283
+
284
+ application session
285
+ ```
286
+
287
+ この middleware では application の認証状態を RPC input の session token から確認します。
288
+
289
+ ```text
290
+ token
291
+
292
+ auth.session.get(token)
293
+
294
+ application Session
295
+ ```
296
+
297
+ Apps Script Active User を application session の代替として扱いません。
298
+
299
+ ## Custom state
300
+
301
+ application 独自の state と組み合わせる場合は `AuthState` と intersection できます。
302
+
303
+ ```ts
304
+ import { authentication, type AuthState } from "@gasboost/auth-app";
305
+
306
+ type AppState = AuthState & {
307
+ requestId: string;
308
+ };
309
+
310
+ const app = new AppsScript<AppState>().use(authentication(auth));
311
+ ```
312
+
313
+ ## Responsibility
314
+
315
+ `@gasboost/auth-app` が担当するもの:
316
+
317
+ - authenticated RPC input の token contract
318
+ - RPC input から token の取得
319
+ - `auth.session.get(token)` による session 検証
320
+ - session の AppsScript state への設定
321
+ - invalid token の拒否
322
+
323
+ 担当しないもの:
324
+
325
+ - session storage の構築
326
+ - repository の構築
327
+ - User の取得
328
+ - authorization
329
+ - `Session.getActiveUser()` による identity 解決
330
+
331
+ これらはそれぞれ `@gasboost/auth` または application の責務です。
@@ -0,0 +1,5 @@
1
+ import type { AppsScriptAuth } from "@gasboost/auth";
2
+ export type AuthSession = NonNullable<Awaited<ReturnType<AppsScriptAuth["session"]["get"]>>>;
3
+ export type AuthState = {
4
+ session: AuthSession;
5
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export type AuthenticatedInput<TInput extends object = Record<never, never>> = TInput & {
2
+ token: string;
3
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ import type { AppsScriptMiddleware } from "@gasboost/app";
2
+ import type { AppsScriptAuth } from "@gasboost/auth";
3
+ import type { AuthState } from "./AuthState";
4
+ export declare function authentication(auth: Pick<AppsScriptAuth, "session">): AppsScriptMiddleware<AuthState>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authentication = authentication;
4
+ function authentication(auth) {
5
+ return async (context, next) => {
6
+ if (context.invocation.type !== "call") {
7
+ return next();
8
+ }
9
+ const input = context.invocation.input;
10
+ if (typeof input !== "object" || input === null || !("token" in input)) {
11
+ return next();
12
+ }
13
+ const token = input.token;
14
+ if (typeof token !== "string") {
15
+ throw new Error("Unauthorized");
16
+ }
17
+ const session = await auth.session.get(token);
18
+ if (session === null) {
19
+ throw new Error("Unauthorized");
20
+ }
21
+ context.state.set("session", session);
22
+ return next();
23
+ };
24
+ }
@@ -0,0 +1,3 @@
1
+ export type { AuthenticatedInput } from "./AuthenticatedInput";
2
+ export { authentication } from "./authentication";
3
+ export type { AuthSession, AuthState } from "./AuthState";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authentication = void 0;
4
+ var authentication_1 = require("./authentication");
5
+ Object.defineProperty(exports, "authentication", { enumerable: true, get: function () { return authentication_1.authentication; } });
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@gasboost/auth-app",
3
+ "version": "0.1.0",
4
+ "description": "Authentication middleware adapter for @gasboost/auth and @gasboost/app.",
5
+ "keywords": [
6
+ "google-apps-script",
7
+ "gas",
8
+ "authentication",
9
+ "auth",
10
+ "middleware",
11
+ "rpc",
12
+ "typescript",
13
+ "gasboost"
14
+ ],
15
+ "license": "MIT",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "default": "./dist/index.js",
25
+ "types": "./dist/index.d.ts"
26
+ }
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/gasboost/auth.git",
31
+ "directory": "packages/auth-app"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/gasboost/auth/issues"
35
+ },
36
+ "homepage": "https://github.com/gasboost/auth#readme",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "peerDependencies": {
41
+ "@gasboost/app": "^3.0.0",
42
+ "@gasboost/auth": "^0.2.0"
43
+ },
44
+ "devDependencies": {
45
+ "@gasboost/app": "^3.0.0",
46
+ "@types/google-apps-script": "^2.0.13",
47
+ "@gasboost/auth": "0.2.1"
48
+ },
49
+ "scripts": {
50
+ "format": "prettier --check . --config ../../.prettierrc.json --ignore-path ../../.prettierignore",
51
+ "format:fix": "prettier --write . --config ../../.prettierrc.json --ignore-path ../../.prettierignore",
52
+ "lint": "eslint . --config ../../eslint.config.mjs",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "build": "rm -rf dist && tsc -p tsconfig.build.json"
56
+ }
57
+ }