@rdlabo/workers-hono-kit 0.3.0 → 0.3.2

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.
@@ -53,6 +53,21 @@ export interface FirebaseVerifier {
53
53
  uid: string;
54
54
  email?: string;
55
55
  } | null>;
56
+ /**
57
+ * Look up multiple user records by uid in as few requests as the backing service allows.
58
+ *
59
+ * Batched equivalent of {@link getUser}, intended to replace N single-uid lookups with a
60
+ * handful of requests.
61
+ *
62
+ * @param uids - The users' unique ids to look up.
63
+ * @returns The `uid`/`email` of every matching user. Uids that do not resolve to a user are
64
+ * simply absent from the result (never `null` entries).
65
+ * @throws If the backing user-management service is not configured or the lookup fails.
66
+ */
67
+ getUsers(uids: string[]): Promise<{
68
+ uid: string;
69
+ email?: string;
70
+ }[]>;
56
71
  /**
57
72
  * Delete a user by uid.
58
73
  *
@@ -48,6 +48,17 @@ export declare class IdentityToolkit {
48
48
  * @internal
49
49
  */
50
50
  private getAccessToken;
51
+ /**
52
+ * Call the `accounts:lookup` endpoint for a single chunk of `localId`s.
53
+ *
54
+ * @param localIds - Up to {@link LOOKUP_CHUNK_SIZE} `localId`s to look up in one request.
55
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
56
+ * @returns The raw `users` entries returned by the endpoint (empty when the request is
57
+ * unsuccessful or no matching users are returned).
58
+ * @throws If acquiring an access token fails.
59
+ * @internal
60
+ */
61
+ private lookupChunk;
51
62
  /**
52
63
  * Look up a user record by uid via the `accounts:lookup` endpoint.
53
64
  *
@@ -61,6 +72,24 @@ export declare class IdentityToolkit {
61
72
  uid: string;
62
73
  email?: string;
63
74
  } | null>;
75
+ /**
76
+ * Look up multiple user records by uid via the `accounts:lookup` endpoint.
77
+ *
78
+ * `uids` are chunked into groups of at most {@link LOOKUP_CHUNK_SIZE} (the maximum `localId`
79
+ * array size the endpoint accepts), issuing one `accounts:lookup` request per chunk. This lets
80
+ * callers replace N single-uid lookups with `ceil(N / LOOKUP_CHUNK_SIZE)` requests.
81
+ *
82
+ * @param uids - The users' unique ids (`localId`s) to look up.
83
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
84
+ * @returns The `uid`/`email` of every matching user. Uids Firebase does not recognize are
85
+ * simply absent from the result (never `null` entries), so callers can treat "missing from
86
+ * the result" as "not found/invalid".
87
+ * @throws If acquiring an access token fails.
88
+ */
89
+ lookupMany(uids: string[], nowSeconds: number): Promise<{
90
+ uid: string;
91
+ email?: string;
92
+ }[]>;
64
93
  /**
65
94
  * Delete a user by uid via the `accounts:delete` endpoint.
66
95
  *
@@ -5,6 +5,8 @@ const TOKEN_URL = 'https://oauth2.googleapis.com/token';
5
5
  const IDENTITY_TOOLKIT = 'https://identitytoolkit.googleapis.com/v1';
6
6
  /** OAuth2 scopes required for Identity Toolkit account lookup and deletion. */
7
7
  const SCOPE = 'https://www.googleapis.com/auth/identitytoolkit https://www.googleapis.com/auth/firebase';
8
+ /** Maximum number of `localId`s the `accounts:lookup` endpoint accepts in a single request. */
9
+ const LOOKUP_CHUNK_SIZE = 100;
8
10
  /**
9
11
  * Minimal Google Identity Toolkit REST client for the user-management operations that token
10
12
  * verification does not cover: `accounts:lookup` (getUser) and `accounts:delete` (deleteUser).
@@ -71,27 +73,68 @@ export class IdentityToolkit {
71
73
  return json.access_token;
72
74
  }
73
75
  /**
74
- * Look up a user record by uid via the `accounts:lookup` endpoint.
76
+ * Call the `accounts:lookup` endpoint for a single chunk of `localId`s.
75
77
  *
76
- * @param uid - The user's unique id (`localId`).
78
+ * @param localIds - Up to {@link LOOKUP_CHUNK_SIZE} `localId`s to look up in one request.
77
79
  * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
78
- * @returns The user's `uid` and optional `email`, or `null` when the request is unsuccessful
79
- * or no matching user is returned.
80
+ * @returns The raw `users` entries returned by the endpoint (empty when the request is
81
+ * unsuccessful or no matching users are returned).
80
82
  * @throws If acquiring an access token fails.
83
+ * @internal
81
84
  */
82
- async lookup(uid, nowSeconds) {
85
+ async lookupChunk(localIds, nowSeconds) {
83
86
  const token = await this.getAccessToken(nowSeconds);
84
87
  const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:lookup`, {
85
88
  method: 'POST',
86
89
  headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
87
- body: JSON.stringify({ localId: [uid] }),
90
+ body: JSON.stringify({ localId: localIds }),
88
91
  });
89
92
  if (!res.ok) {
90
- return null;
93
+ return [];
91
94
  }
92
95
  const json = (await res.json());
93
- const user = json.users?.[0];
94
- return user ? { uid: user.localId, email: user.email } : null;
96
+ return json.users ?? [];
97
+ }
98
+ /**
99
+ * Look up a user record by uid via the `accounts:lookup` endpoint.
100
+ *
101
+ * @param uid - The user's unique id (`localId`).
102
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
103
+ * @returns The user's `uid` and optional `email`, or `null` when the request is unsuccessful
104
+ * or no matching user is returned.
105
+ * @throws If acquiring an access token fails.
106
+ */
107
+ async lookup(uid, nowSeconds) {
108
+ const users = await this.lookupChunk([uid], nowSeconds);
109
+ return users.length > 0 ? { uid: users[0].localId, email: users[0].email } : null;
110
+ }
111
+ /**
112
+ * Look up multiple user records by uid via the `accounts:lookup` endpoint.
113
+ *
114
+ * `uids` are chunked into groups of at most {@link LOOKUP_CHUNK_SIZE} (the maximum `localId`
115
+ * array size the endpoint accepts), issuing one `accounts:lookup` request per chunk. This lets
116
+ * callers replace N single-uid lookups with `ceil(N / LOOKUP_CHUNK_SIZE)` requests.
117
+ *
118
+ * @param uids - The users' unique ids (`localId`s) to look up.
119
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
120
+ * @returns The `uid`/`email` of every matching user. Uids Firebase does not recognize are
121
+ * simply absent from the result (never `null` entries), so callers can treat "missing from
122
+ * the result" as "not found/invalid".
123
+ * @throws If acquiring an access token fails.
124
+ */
125
+ async lookupMany(uids, nowSeconds) {
126
+ if (uids.length === 0) {
127
+ return [];
128
+ }
129
+ const results = [];
130
+ for (let i = 0; i < uids.length; i += LOOKUP_CHUNK_SIZE) {
131
+ const chunk = uids.slice(i, i + LOOKUP_CHUNK_SIZE);
132
+ const users = await this.lookupChunk(chunk, nowSeconds);
133
+ for (const user of users) {
134
+ results.push({ uid: user.localId, email: user.email });
135
+ }
136
+ }
137
+ return results;
95
138
  }
96
139
  /**
97
140
  * Delete a user by uid via the `accounts:delete` endpoint.
@@ -85,6 +85,21 @@ export declare class JoseFirebaseVerifier implements FirebaseVerifier {
85
85
  uid: string;
86
86
  email?: string;
87
87
  } | null>;
88
+ /**
89
+ * Look up multiple user records by uid via the Identity Toolkit REST API.
90
+ *
91
+ * Batches the lookups into `ceil(uids.length / 100)` `accounts:lookup` requests instead of
92
+ * one request per uid.
93
+ *
94
+ * @param uids - The users' unique ids to look up.
95
+ * @returns The `uid`/`email` of every matching user. Uids Firebase does not recognize are
96
+ * simply absent from the result (never `null` entries).
97
+ * @throws If no Identity Toolkit client was configured on this verifier.
98
+ */
99
+ getUsers(uids: string[]): Promise<{
100
+ uid: string;
101
+ email?: string;
102
+ }[]>;
88
103
  /**
89
104
  * Delete a user by uid via the Identity Toolkit REST API.
90
105
  *
@@ -88,6 +88,23 @@ export class JoseFirebaseVerifier {
88
88
  }
89
89
  return this.opts.identity.lookup(uid, this.nowSeconds());
90
90
  }
91
+ /**
92
+ * Look up multiple user records by uid via the Identity Toolkit REST API.
93
+ *
94
+ * Batches the lookups into `ceil(uids.length / 100)` `accounts:lookup` requests instead of
95
+ * one request per uid.
96
+ *
97
+ * @param uids - The users' unique ids to look up.
98
+ * @returns The `uid`/`email` of every matching user. Uids Firebase does not recognize are
99
+ * simply absent from the result (never `null` entries).
100
+ * @throws If no Identity Toolkit client was configured on this verifier.
101
+ */
102
+ async getUsers(uids) {
103
+ if (!this.opts.identity) {
104
+ throw new Error('Identity Toolkit not configured');
105
+ }
106
+ return this.opts.identity.lookupMany(uids, this.nowSeconds());
107
+ }
91
108
  /**
92
109
  * Delete a user by uid via the Identity Toolkit REST API.
93
110
  *
@@ -43,6 +43,16 @@ export declare class FakeFirebaseVerifier implements FirebaseVerifier {
43
43
  uid: string;
44
44
  email?: string;
45
45
  } | null>;
46
+ /**
47
+ * Return minimal user records echoing each requested UID.
48
+ *
49
+ * @param uids - UIDs to look up.
50
+ * @returns One `{ uid }` entry per requested uid (never omits any, in this fake).
51
+ */
52
+ getUsers(uids: string[]): Promise<{
53
+ uid: string;
54
+ email?: string;
55
+ }[]>;
46
56
  /**
47
57
  * Record a user deletion by appending the UID to {@link FakeFirebaseVerifier.deleted}.
48
58
  *
@@ -48,6 +48,15 @@ export class FakeFirebaseVerifier {
48
48
  async getUser(uid) {
49
49
  return { uid };
50
50
  }
51
+ /**
52
+ * Return minimal user records echoing each requested UID.
53
+ *
54
+ * @param uids - UIDs to look up.
55
+ * @returns One `{ uid }` entry per requested uid (never omits any, in this fake).
56
+ */
57
+ async getUsers(uids) {
58
+ return uids.map((uid) => ({ uid }));
59
+ }
51
60
  /**
52
61
  * Record a user deletion by appending the UID to {@link FakeFirebaseVerifier.deleted}.
53
62
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -51,6 +51,18 @@ export interface FirebaseVerifier {
51
51
  * @throws If the backing user-management service is not configured or the lookup fails.
52
52
  */
53
53
  getUser(uid: string): Promise<{ uid: string; email?: string } | null>;
54
+ /**
55
+ * Look up multiple user records by uid in as few requests as the backing service allows.
56
+ *
57
+ * Batched equivalent of {@link getUser}, intended to replace N single-uid lookups with a
58
+ * handful of requests.
59
+ *
60
+ * @param uids - The users' unique ids to look up.
61
+ * @returns The `uid`/`email` of every matching user. Uids that do not resolve to a user are
62
+ * simply absent from the result (never `null` entries).
63
+ * @throws If the backing user-management service is not configured or the lookup fails.
64
+ */
65
+ getUsers(uids: string[]): Promise<{ uid: string; email?: string }[]>;
54
66
  /**
55
67
  * Delete a user by uid.
56
68
  *
@@ -21,6 +21,8 @@ const TOKEN_URL = 'https://oauth2.googleapis.com/token';
21
21
  const IDENTITY_TOOLKIT = 'https://identitytoolkit.googleapis.com/v1';
22
22
  /** OAuth2 scopes required for Identity Toolkit account lookup and deletion. */
23
23
  const SCOPE = 'https://www.googleapis.com/auth/identitytoolkit https://www.googleapis.com/auth/firebase';
24
+ /** Maximum number of `localId`s the `accounts:lookup` endpoint accepts in a single request. */
25
+ const LOOKUP_CHUNK_SIZE = 100;
24
26
 
25
27
  /**
26
28
  * Minimal Google Identity Toolkit REST client for the user-management operations that token
@@ -89,27 +91,70 @@ export class IdentityToolkit {
89
91
  }
90
92
 
91
93
  /**
92
- * Look up a user record by uid via the `accounts:lookup` endpoint.
94
+ * Call the `accounts:lookup` endpoint for a single chunk of `localId`s.
93
95
  *
94
- * @param uid - The user's unique id (`localId`).
96
+ * @param localIds - Up to {@link LOOKUP_CHUNK_SIZE} `localId`s to look up in one request.
95
97
  * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
96
- * @returns The user's `uid` and optional `email`, or `null` when the request is unsuccessful
97
- * or no matching user is returned.
98
+ * @returns The raw `users` entries returned by the endpoint (empty when the request is
99
+ * unsuccessful or no matching users are returned).
98
100
  * @throws If acquiring an access token fails.
101
+ * @internal
99
102
  */
100
- async lookup(uid: string, nowSeconds: number): Promise<{ uid: string; email?: string } | null> {
103
+ private async lookupChunk(localIds: string[], nowSeconds: number): Promise<{ localId: string; email?: string }[]> {
101
104
  const token = await this.getAccessToken(nowSeconds);
102
105
  const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:lookup`, {
103
106
  method: 'POST',
104
107
  headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
105
- body: JSON.stringify({ localId: [uid] }),
108
+ body: JSON.stringify({ localId: localIds }),
106
109
  });
107
110
  if (!res.ok) {
108
- return null;
111
+ return [];
109
112
  }
110
113
  const json = (await res.json()) as { users?: { localId: string; email?: string }[] };
111
- const user = json.users?.[0];
112
- return user ? { uid: user.localId, email: user.email } : null;
114
+ return json.users ?? [];
115
+ }
116
+
117
+ /**
118
+ * Look up a user record by uid via the `accounts:lookup` endpoint.
119
+ *
120
+ * @param uid - The user's unique id (`localId`).
121
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
122
+ * @returns The user's `uid` and optional `email`, or `null` when the request is unsuccessful
123
+ * or no matching user is returned.
124
+ * @throws If acquiring an access token fails.
125
+ */
126
+ async lookup(uid: string, nowSeconds: number): Promise<{ uid: string; email?: string } | null> {
127
+ const users = await this.lookupChunk([uid], nowSeconds);
128
+ return users.length > 0 ? { uid: users[0].localId, email: users[0].email } : null;
129
+ }
130
+
131
+ /**
132
+ * Look up multiple user records by uid via the `accounts:lookup` endpoint.
133
+ *
134
+ * `uids` are chunked into groups of at most {@link LOOKUP_CHUNK_SIZE} (the maximum `localId`
135
+ * array size the endpoint accepts), issuing one `accounts:lookup` request per chunk. This lets
136
+ * callers replace N single-uid lookups with `ceil(N / LOOKUP_CHUNK_SIZE)` requests.
137
+ *
138
+ * @param uids - The users' unique ids (`localId`s) to look up.
139
+ * @param nowSeconds - The current Unix time in seconds, used for access-token caching.
140
+ * @returns The `uid`/`email` of every matching user. Uids Firebase does not recognize are
141
+ * simply absent from the result (never `null` entries), so callers can treat "missing from
142
+ * the result" as "not found/invalid".
143
+ * @throws If acquiring an access token fails.
144
+ */
145
+ async lookupMany(uids: string[], nowSeconds: number): Promise<{ uid: string; email?: string }[]> {
146
+ if (uids.length === 0) {
147
+ return [];
148
+ }
149
+ const results: { uid: string; email?: string }[] = [];
150
+ for (let i = 0; i < uids.length; i += LOOKUP_CHUNK_SIZE) {
151
+ const chunk = uids.slice(i, i + LOOKUP_CHUNK_SIZE);
152
+ const users = await this.lookupChunk(chunk, nowSeconds);
153
+ for (const user of users) {
154
+ results.push({ uid: user.localId, email: user.email });
155
+ }
156
+ }
157
+ return results;
113
158
  }
114
159
 
115
160
  /**
@@ -115,6 +115,24 @@ export class JoseFirebaseVerifier implements FirebaseVerifier {
115
115
  return this.opts.identity.lookup(uid, this.nowSeconds());
116
116
  }
117
117
 
118
+ /**
119
+ * Look up multiple user records by uid via the Identity Toolkit REST API.
120
+ *
121
+ * Batches the lookups into `ceil(uids.length / 100)` `accounts:lookup` requests instead of
122
+ * one request per uid.
123
+ *
124
+ * @param uids - The users' unique ids to look up.
125
+ * @returns The `uid`/`email` of every matching user. Uids Firebase does not recognize are
126
+ * simply absent from the result (never `null` entries).
127
+ * @throws If no Identity Toolkit client was configured on this verifier.
128
+ */
129
+ async getUsers(uids: string[]): Promise<{ uid: string; email?: string }[]> {
130
+ if (!this.opts.identity) {
131
+ throw new Error('Identity Toolkit not configured');
132
+ }
133
+ return this.opts.identity.lookupMany(uids, this.nowSeconds());
134
+ }
135
+
118
136
  /**
119
137
  * Delete a user by uid via the Identity Toolkit REST API.
120
138
  *
@@ -56,6 +56,16 @@ export class FakeFirebaseVerifier implements FirebaseVerifier {
56
56
  return { uid };
57
57
  }
58
58
 
59
+ /**
60
+ * Return minimal user records echoing each requested UID.
61
+ *
62
+ * @param uids - UIDs to look up.
63
+ * @returns One `{ uid }` entry per requested uid (never omits any, in this fake).
64
+ */
65
+ async getUsers(uids: string[]): Promise<{ uid: string; email?: string }[]> {
66
+ return uids.map((uid) => ({ uid }));
67
+ }
68
+
59
69
  /**
60
70
  * Record a user deletion by appending the UID to {@link FakeFirebaseVerifier.deleted}.
61
71
  *