@rdlabo/workers-hono-kit 0.3.0 → 0.3.3
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/dist/firebase/firebase-verifier.d.ts +15 -0
- package/dist/firebase/identity-toolkit.d.ts +29 -0
- package/dist/firebase/identity-toolkit.js +52 -9
- package/dist/firebase/jose-firebase-verifier.d.ts +15 -0
- package/dist/firebase/jose-firebase-verifier.js +17 -0
- package/dist/http/trailing-slash.d.ts +21 -0
- package/dist/http/trailing-slash.js +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/testing/fakes.d.ts +10 -0
- package/dist/testing/fakes.js +9 -0
- package/package.json +1 -1
- package/src/firebase/firebase-verifier.ts +12 -0
- package/src/firebase/identity-toolkit.ts +54 -9
- package/src/firebase/jose-firebase-verifier.ts +18 -0
- package/src/http/trailing-slash.ts +28 -0
- package/src/index.ts +1 -0
- package/src/testing/fakes.ts +10 -0
|
@@ -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
|
-
*
|
|
76
|
+
* Call the `accounts:lookup` endpoint for a single chunk of `localId`s.
|
|
75
77
|
*
|
|
76
|
-
* @param
|
|
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
|
|
79
|
-
* or no matching
|
|
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
|
|
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:
|
|
90
|
+
body: JSON.stringify({ localId: localIds }),
|
|
88
91
|
});
|
|
89
92
|
if (!res.ok) {
|
|
90
|
-
return
|
|
93
|
+
return [];
|
|
91
94
|
}
|
|
92
95
|
const json = (await res.json());
|
|
93
|
-
|
|
94
|
-
|
|
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
|
*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trailing-slash normalization for NestJS(Express) → Hono parity.
|
|
3
|
+
*
|
|
4
|
+
* Express runs with strict routing off by default, so `/x` and `/x/` resolve to the same handler.
|
|
5
|
+
* Hono distinguishes them, so a client that sends e.g. `GET /functions/timeline/` 404s against a
|
|
6
|
+
* Hono worker that only registered `/functions/timeline`. Apply this at the Worker `fetch` entry to
|
|
7
|
+
* strip a trailing slash before routing, preserving method / headers / body.
|
|
8
|
+
*
|
|
9
|
+
* We intentionally do NOT use `hono/trailing-slash`'s `trimTrailingSlash`, which responds with a 301
|
|
10
|
+
* redirect and thus breaks non-GET methods (POST/PUT/DELETE) and clients that don't follow redirects.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* export default {
|
|
14
|
+
* fetch: (request, env, ctx) => app.fetch(normalizeTrailingSlash(request), env, ctx),
|
|
15
|
+
* };
|
|
16
|
+
*
|
|
17
|
+
* @param request - The incoming request.
|
|
18
|
+
* @returns The same request when the path has no trailing slash (or is exactly `/`), otherwise a new
|
|
19
|
+
* Request with the trailing slash(es) removed.
|
|
20
|
+
*/
|
|
21
|
+
export declare const normalizeTrailingSlash: (request: Request) => Request;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trailing-slash normalization for NestJS(Express) → Hono parity.
|
|
3
|
+
*
|
|
4
|
+
* Express runs with strict routing off by default, so `/x` and `/x/` resolve to the same handler.
|
|
5
|
+
* Hono distinguishes them, so a client that sends e.g. `GET /functions/timeline/` 404s against a
|
|
6
|
+
* Hono worker that only registered `/functions/timeline`. Apply this at the Worker `fetch` entry to
|
|
7
|
+
* strip a trailing slash before routing, preserving method / headers / body.
|
|
8
|
+
*
|
|
9
|
+
* We intentionally do NOT use `hono/trailing-slash`'s `trimTrailingSlash`, which responds with a 301
|
|
10
|
+
* redirect and thus breaks non-GET methods (POST/PUT/DELETE) and clients that don't follow redirects.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* export default {
|
|
14
|
+
* fetch: (request, env, ctx) => app.fetch(normalizeTrailingSlash(request), env, ctx),
|
|
15
|
+
* };
|
|
16
|
+
*
|
|
17
|
+
* @param request - The incoming request.
|
|
18
|
+
* @returns The same request when the path has no trailing slash (or is exactly `/`), otherwise a new
|
|
19
|
+
* Request with the trailing slash(es) removed.
|
|
20
|
+
*/
|
|
21
|
+
export const normalizeTrailingSlash = (request) => {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
if (url.pathname.length > 1 && url.pathname.endsWith('/')) {
|
|
24
|
+
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
25
|
+
return new Request(url, request);
|
|
26
|
+
}
|
|
27
|
+
return request;
|
|
28
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export type { AppEnv } from './http/app-env.js';
|
|
|
24
24
|
export { HttpStatus } from './http/http-status.js';
|
|
25
25
|
export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error.js';
|
|
26
26
|
export type { NestErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/nest-error.js';
|
|
27
|
+
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
27
28
|
export { KVCache } from './cache/kv-cache.js';
|
|
28
29
|
export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
|
|
29
30
|
export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { getAppInfo } from './http/app-info.js';
|
|
|
20
20
|
export { resolveAppEnv, isProductionEnv } from './http/app-env.js';
|
|
21
21
|
export { HttpStatus } from './http/http-status.js';
|
|
22
22
|
export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error.js';
|
|
23
|
+
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
23
24
|
// cache
|
|
24
25
|
export { KVCache } from './cache/kv-cache.js';
|
|
25
26
|
// stripe
|
package/dist/testing/fakes.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/testing/fakes.js
CHANGED
|
@@ -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
|
@@ -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
|
-
*
|
|
94
|
+
* Call the `accounts:lookup` endpoint for a single chunk of `localId`s.
|
|
93
95
|
*
|
|
94
|
-
* @param
|
|
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
|
|
97
|
-
* or no matching
|
|
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
|
|
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:
|
|
108
|
+
body: JSON.stringify({ localId: localIds }),
|
|
106
109
|
});
|
|
107
110
|
if (!res.ok) {
|
|
108
|
-
return
|
|
111
|
+
return [];
|
|
109
112
|
}
|
|
110
113
|
const json = (await res.json()) as { users?: { localId: string; email?: string }[] };
|
|
111
|
-
|
|
112
|
-
|
|
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
|
*
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trailing-slash normalization for NestJS(Express) → Hono parity.
|
|
3
|
+
*
|
|
4
|
+
* Express runs with strict routing off by default, so `/x` and `/x/` resolve to the same handler.
|
|
5
|
+
* Hono distinguishes them, so a client that sends e.g. `GET /functions/timeline/` 404s against a
|
|
6
|
+
* Hono worker that only registered `/functions/timeline`. Apply this at the Worker `fetch` entry to
|
|
7
|
+
* strip a trailing slash before routing, preserving method / headers / body.
|
|
8
|
+
*
|
|
9
|
+
* We intentionally do NOT use `hono/trailing-slash`'s `trimTrailingSlash`, which responds with a 301
|
|
10
|
+
* redirect and thus breaks non-GET methods (POST/PUT/DELETE) and clients that don't follow redirects.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* export default {
|
|
14
|
+
* fetch: (request, env, ctx) => app.fetch(normalizeTrailingSlash(request), env, ctx),
|
|
15
|
+
* };
|
|
16
|
+
*
|
|
17
|
+
* @param request - The incoming request.
|
|
18
|
+
* @returns The same request when the path has no trailing slash (or is exactly `/`), otherwise a new
|
|
19
|
+
* Request with the trailing slash(es) removed.
|
|
20
|
+
*/
|
|
21
|
+
export const normalizeTrailingSlash = (request: Request): Request => {
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
if (url.pathname.length > 1 && url.pathname.endsWith('/')) {
|
|
24
|
+
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
25
|
+
return new Request(url, request);
|
|
26
|
+
}
|
|
27
|
+
return request;
|
|
28
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type { AppEnv } from './http/app-env.js';
|
|
|
34
34
|
export { HttpStatus } from './http/http-status.js';
|
|
35
35
|
export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error.js';
|
|
36
36
|
export type { NestErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/nest-error.js';
|
|
37
|
+
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
37
38
|
|
|
38
39
|
// cache
|
|
39
40
|
export { KVCache } from './cache/kv-cache.js';
|
package/src/testing/fakes.ts
CHANGED
|
@@ -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
|
*
|