@rdlabo/workers-hono-kit 0.1.0 → 0.2.1
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 +126 -11
- package/dist/ai/gateway.d.ts +54 -16
- package/dist/ai/gateway.js +37 -12
- package/dist/aws/cloudfront.d.ts +23 -5
- package/dist/aws/cloudfront.js +45 -6
- package/dist/aws/secrets-manager.d.ts +38 -4
- package/dist/aws/secrets-manager.js +48 -3
- package/dist/cache/kv-cache.d.ts +173 -10
- package/dist/cache/kv-cache.js +139 -7
- package/dist/db/connection.d.ts +56 -14
- package/dist/db/connection.js +39 -13
- package/dist/db/database.d.ts +160 -24
- package/dist/db/database.js +51 -7
- package/dist/db/index.d.ts +21 -10
- package/dist/db/index.js +17 -8
- package/dist/db/jst.d.ts +89 -6
- package/dist/db/jst.js +89 -23
- package/dist/db/orm-config.d.ts +61 -19
- package/dist/db/orm-config.js +43 -14
- package/dist/db/retry.d.ts +25 -3
- package/dist/db/retry.js +25 -3
- package/dist/db/write-result.d.ts +27 -4
- package/dist/db/write-result.js +22 -1
- package/dist/firebase/firebase-verifier.d.ts +53 -4
- package/dist/firebase/identity-toolkit.d.ts +54 -5
- package/dist/firebase/identity-toolkit.js +51 -0
- package/dist/firebase/jose-firebase-verifier.d.ts +81 -9
- package/dist/firebase/jose-firebase-verifier.js +68 -7
- package/dist/firebase/remote-verifier.d.ts +43 -5
- package/dist/firebase/remote-verifier.js +60 -11
- package/dist/http/app-env.d.ts +41 -8
- package/dist/http/app-env.js +38 -8
- package/dist/http/app-info.d.ts +25 -3
- package/dist/http/app-info.js +16 -2
- package/dist/http/http-status.d.ts +12 -3
- package/dist/http/http-status.js +12 -3
- package/dist/http/nest-error.d.ts +90 -29
- package/dist/http/nest-error.js +59 -18
- package/dist/http/user-protocol.d.ts +23 -3
- package/dist/http/user-protocol.js +14 -2
- package/dist/index.d.ts +41 -30
- package/dist/index.js +29 -21
- package/dist/middleware/auth.d.ts +75 -14
- package/dist/middleware/auth.js +31 -7
- package/dist/middleware/finalize-response.d.ts +30 -0
- package/dist/middleware/finalize-response.js +41 -12
- package/dist/middleware/validation.d.ts +83 -9
- package/dist/middleware/validation.js +52 -9
- package/dist/middleware/zod-coerce.d.ts +56 -2
- package/dist/middleware/zod-coerce.js +68 -9
- package/dist/stripe/client.d.ts +46 -9
- package/dist/stripe/client.js +41 -3
- package/dist/testing/auth.d.ts +60 -12
- package/dist/testing/auth.js +58 -10
- package/dist/testing/configurable-fake.d.ts +20 -9
- package/dist/testing/configurable-fake.js +23 -11
- package/dist/testing/db.d.ts +81 -12
- package/dist/testing/db.js +23 -1
- package/dist/testing/fakes.d.ts +79 -11
- package/dist/testing/fakes.js +70 -8
- package/dist/testing/index.d.ts +15 -8
- package/dist/testing/index.js +15 -10
- package/dist/testing/stripe-fixtures.d.ts +93 -3
- package/dist/testing/stripe-fixtures.js +93 -3
- package/package.json +19 -9
- package/src/ai/gateway.ts +66 -27
- package/src/aws/cloudfront.ts +46 -6
- package/src/aws/secrets-manager.ts +56 -7
- package/src/cache/kv-cache.ts +194 -12
- package/src/db/connection.ts +56 -14
- package/src/db/database.ts +163 -27
- package/src/db/index.ts +21 -12
- package/src/db/jst.ts +89 -23
- package/src/db/orm-config.ts +61 -19
- package/src/db/retry.ts +25 -3
- package/src/db/write-result.ts +27 -4
- package/src/firebase/firebase-verifier.ts +53 -4
- package/src/firebase/identity-toolkit.ts +57 -5
- package/src/firebase/jose-firebase-verifier.ts +81 -11
- package/src/firebase/remote-verifier.ts +61 -12
- package/src/http/app-env.ts +41 -8
- package/src/http/app-info.ts +25 -3
- package/src/http/http-status.ts +12 -3
- package/src/http/nest-error.ts +106 -37
- package/src/http/user-protocol.ts +23 -3
- package/src/index.ts +47 -33
- package/src/middleware/auth.ts +79 -17
- package/src/middleware/finalize-response.ts +41 -12
- package/src/middleware/validation.ts +89 -15
- package/src/middleware/zod-coerce.ts +68 -9
- package/src/stripe/client.ts +46 -9
- package/src/testing/auth.ts +60 -12
- package/src/testing/configurable-fake.ts +23 -11
- package/src/testing/db.ts +82 -13
- package/src/testing/fakes.ts +80 -12
- package/src/testing/index.ts +18 -13
- package/src/testing/stripe-fixtures.ts +93 -3
|
@@ -1,15 +1,64 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Decoded Firebase ID token payload.
|
|
3
|
+
*
|
|
4
|
+
* Shaped to match the subset of `firebase-admin`'s `DecodedIdToken` that consumers
|
|
5
|
+
* typically rely on: a stable `uid` plus an optional `email`. The index signature keeps
|
|
6
|
+
* every other JWT claim (e.g. `name`, `picture`, custom claims) accessible without
|
|
7
|
+
* enumerating them here.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* This is the return type of {@link FirebaseVerifier.verifyIdToken}. The `uid` is derived
|
|
11
|
+
* from the token's `sub` claim.
|
|
12
|
+
*/
|
|
2
13
|
export interface DecodedIdToken {
|
|
14
|
+
/** The authenticated user's unique id, taken from the token's `sub` claim. */
|
|
3
15
|
uid: string;
|
|
16
|
+
/** The user's email address, when present on the token. */
|
|
4
17
|
email?: string;
|
|
18
|
+
/** Any additional JWT claim carried by the token (custom claims, `name`, `picture`, ...). */
|
|
5
19
|
[claim: string]: unknown;
|
|
6
20
|
}
|
|
7
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Abstract authentication boundary that replaces the `firebase-admin` Auth surface
|
|
24
|
+
* (`verifyIdToken` / `getUser` / `deleteUser`) for environments where the Node SDK cannot
|
|
25
|
+
* run, such as Cloudflare Workers.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Implementations verify Firebase ID tokens and look up or delete accounts without the
|
|
29
|
+
* `firebase-admin` Node dependency. See `JoseFirebaseVerifier` for the `jose`-based
|
|
30
|
+
* implementation and the `createRemoteFirebaseVerifier` / `createServiceAccountVerifier`
|
|
31
|
+
* factories for ready-made instances.
|
|
32
|
+
*/
|
|
8
33
|
export interface FirebaseVerifier {
|
|
9
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Verify a Firebase ID token and return its decoded payload.
|
|
36
|
+
*
|
|
37
|
+
* Mirrors `firebase-admin` `getAuth().verifyIdToken()`.
|
|
38
|
+
*
|
|
39
|
+
* @param idToken - The raw Firebase ID token (JWT) to verify.
|
|
40
|
+
* @returns The decoded token payload.
|
|
41
|
+
* @throws If the token signature, issuer, audience, expiry, or other required claims are invalid.
|
|
42
|
+
*/
|
|
10
43
|
verifyIdToken(idToken: string): Promise<DecodedIdToken>;
|
|
11
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Look up a user record by uid.
|
|
46
|
+
*
|
|
47
|
+
* Mirrors `firebase-admin` `getAuth().getUser()`.
|
|
48
|
+
*
|
|
49
|
+
* @param uid - The user's unique id.
|
|
50
|
+
* @returns The user's `uid` and optional `email`, or `null` when the user does not exist.
|
|
51
|
+
* @throws If the backing user-management service is not configured or the lookup fails.
|
|
52
|
+
*/
|
|
12
53
|
getUser(uid: string): Promise<{ uid: string; email?: string } | null>;
|
|
13
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Delete a user by uid.
|
|
56
|
+
*
|
|
57
|
+
* Mirrors `firebase-admin` `getAuth().deleteUser()`.
|
|
58
|
+
*
|
|
59
|
+
* @param uid - The user's unique id.
|
|
60
|
+
* @returns A promise that resolves once the user has been deleted.
|
|
61
|
+
* @throws If the backing user-management service is not configured or the deletion fails.
|
|
62
|
+
*/
|
|
14
63
|
deleteUser(uid: string): Promise<void>;
|
|
15
64
|
}
|
|
@@ -1,28 +1,63 @@
|
|
|
1
1
|
import { SignJWT, importPKCS8 } from 'jose';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Minimal
|
|
5
|
-
* that aren't token verification: accounts:lookup (getUser) and accounts:delete
|
|
6
|
-
* (deleteUser). Replaces the firebase-admin Node SDK, which won't run on workerd.
|
|
4
|
+
* Minimal service-account credential consumed by {@link IdentityToolkit}.
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Corresponds to the relevant fields of a Google service-account JSON key file.
|
|
10
8
|
*/
|
|
11
9
|
export interface ServiceAccount {
|
|
10
|
+
/** The service account's email, used as the JWT assertion issuer and subject. */
|
|
12
11
|
client_email: string;
|
|
12
|
+
/** The PEM-encoded PKCS#8 RSA private key used to sign the OAuth2 assertion. */
|
|
13
13
|
private_key: string;
|
|
14
|
+
/** The Google/Firebase project id the Identity Toolkit calls target. */
|
|
14
15
|
project_id: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/** Google OAuth2 token endpoint used to exchange a signed JWT assertion for an access token. */
|
|
17
19
|
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
20
|
+
/** Base URL of the Google Identity Toolkit v1 REST API. */
|
|
18
21
|
const IDENTITY_TOOLKIT = 'https://identitytoolkit.googleapis.com/v1';
|
|
22
|
+
/** OAuth2 scopes required for Identity Toolkit account lookup and deletion. */
|
|
19
23
|
const SCOPE = 'https://www.googleapis.com/auth/identitytoolkit https://www.googleapis.com/auth/firebase';
|
|
20
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Minimal Google Identity Toolkit REST client for the user-management operations that token
|
|
27
|
+
* verification does not cover: `accounts:lookup` (getUser) and `accounts:delete` (deleteUser).
|
|
28
|
+
*
|
|
29
|
+
* This replaces the parts of the `firebase-admin` Node SDK that cannot run on Cloudflare
|
|
30
|
+
* Workers (workerd).
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Authentication follows the JWT-bearer flow: a JWT assertion is signed with the service
|
|
34
|
+
* account's private key (via `jose`), exchanged at the OAuth2 token endpoint for an access
|
|
35
|
+
* token, and that token is then used to call the REST API. Access tokens are cached in-process
|
|
36
|
+
* and reused until shortly before they expire.
|
|
37
|
+
*/
|
|
21
38
|
export class IdentityToolkit {
|
|
39
|
+
/** Cached OAuth2 access token and its absolute expiry (Unix seconds), or `null` when none. */
|
|
22
40
|
private accessToken: { value: string; expiresAt: number } | null = null;
|
|
23
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Create a client bound to a single service account.
|
|
44
|
+
*
|
|
45
|
+
* @param sa - The service-account credential used to authenticate REST calls.
|
|
46
|
+
*/
|
|
24
47
|
constructor(private readonly sa: ServiceAccount) {}
|
|
25
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Return a valid OAuth2 access token, minting a new one when the cache is empty or expiring.
|
|
51
|
+
*
|
|
52
|
+
* Signs a short-lived JWT assertion with the service-account key and exchanges it at the
|
|
53
|
+
* Google OAuth2 token endpoint. The result is cached and reused while it remains valid
|
|
54
|
+
* (with a 60-second safety margin).
|
|
55
|
+
*
|
|
56
|
+
* @param nowSeconds - The current Unix time in seconds, used for cache validity and JWT timestamps.
|
|
57
|
+
* @returns A bearer access token for the Identity Toolkit API.
|
|
58
|
+
* @throws If the token exchange request fails.
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
26
61
|
private async getAccessToken(nowSeconds: number): Promise<string> {
|
|
27
62
|
if (this.accessToken && this.accessToken.expiresAt > nowSeconds + 60) {
|
|
28
63
|
return this.accessToken.value;
|
|
@@ -53,6 +88,15 @@ export class IdentityToolkit {
|
|
|
53
88
|
return json.access_token;
|
|
54
89
|
}
|
|
55
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Look up a user record by uid via the `accounts:lookup` endpoint.
|
|
93
|
+
*
|
|
94
|
+
* @param uid - The user's unique id (`localId`).
|
|
95
|
+
* @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
|
+
* @throws If acquiring an access token fails.
|
|
99
|
+
*/
|
|
56
100
|
async lookup(uid: string, nowSeconds: number): Promise<{ uid: string; email?: string } | null> {
|
|
57
101
|
const token = await this.getAccessToken(nowSeconds);
|
|
58
102
|
const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:lookup`, {
|
|
@@ -68,6 +112,14 @@ export class IdentityToolkit {
|
|
|
68
112
|
return user ? { uid: user.localId, email: user.email } : null;
|
|
69
113
|
}
|
|
70
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Delete a user by uid via the `accounts:delete` endpoint.
|
|
117
|
+
*
|
|
118
|
+
* @param uid - The user's unique id (`localId`).
|
|
119
|
+
* @param nowSeconds - The current Unix time in seconds, used for access-token caching.
|
|
120
|
+
* @returns A promise that resolves once the user has been deleted.
|
|
121
|
+
* @throws If acquiring an access token fails or the delete request is unsuccessful.
|
|
122
|
+
*/
|
|
71
123
|
async remove(uid: string, nowSeconds: number): Promise<void> {
|
|
72
124
|
const token = await this.getAccessToken(nowSeconds);
|
|
73
125
|
const res = await fetch(`${IDENTITY_TOOLKIT}/projects/${this.sa.project_id}/accounts:delete`, {
|
|
@@ -1,26 +1,64 @@
|
|
|
1
1
|
import { jwtVerify } from 'jose';
|
|
2
2
|
import type { CryptoKey, JWK, JWTVerifyGetKey, KeyObject } from 'jose';
|
|
3
|
-
import type { DecodedIdToken, FirebaseVerifier } from './firebase-verifier';
|
|
4
|
-
import type { IdentityToolkit } from './identity-toolkit';
|
|
3
|
+
import type { DecodedIdToken, FirebaseVerifier } from './firebase-verifier.js';
|
|
4
|
+
import type { IdentityToolkit } from './identity-toolkit.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Union of every key shape `jose`'s `jwtVerify` accepts.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* `jose` v6 removed `KeyLike`, so the verification key is modelled here as either a static
|
|
11
|
+
* key (production uses `createRemoteJWKSet`, tests use a generated `CryptoKey`) or a dynamic
|
|
12
|
+
* `JWTVerifyGetKey` resolver function. This union covers both `jwtVerify` overloads' key
|
|
13
|
+
* parameters.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
8
17
|
type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array | JWTVerifyGetKey;
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* RS256, a non-empty subject (the uid), and a valid auth_time.
|
|
14
|
-
*
|
|
15
|
-
* - prod: keyResolver = createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL)).
|
|
16
|
-
* - test: keyResolver = the generated public key (offline, no network).
|
|
20
|
+
* URL of Google's securetoken JWKS endpoint, which serves the public keys used to sign
|
|
21
|
+
* Firebase ID tokens.
|
|
17
22
|
*
|
|
18
|
-
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* Passed to `createRemoteJWKSet` (see `createRemoteFirebaseVerifier`) so RS256 signatures can
|
|
25
|
+
* be verified against Google's rotating public keys.
|
|
19
26
|
*/
|
|
20
27
|
export const SECURETOKEN_JWK_URL =
|
|
21
28
|
'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com';
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Verifies Firebase ID tokens with `jose` RS256 against Google's securetoken JWKS, and
|
|
32
|
+
* optionally looks up or deletes users via the Google Identity Toolkit REST API.
|
|
33
|
+
*
|
|
34
|
+
* This replaces the `firebase-admin` Auth surface (`verifyIdToken` / `getUser` /
|
|
35
|
+
* `deleteUser`) in environments where the Node SDK cannot run, such as Cloudflare Workers.
|
|
36
|
+
* Token verification mirrors the admin SDK's checks: issuer and audience equal to the
|
|
37
|
+
* project id, an RS256 signature, a non-empty subject (the uid), and a valid `auth_time`.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* The verification key is supplied as `keyResolver`:
|
|
41
|
+
* - Production: `createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL))`, which fetches and caches
|
|
42
|
+
* Google's public keys.
|
|
43
|
+
* - Tests: a generated public key, allowing fully offline verification with no network.
|
|
44
|
+
*
|
|
45
|
+
* `getUser` / `deleteUser` delegate to {@link IdentityToolkit} (a network call); when no
|
|
46
|
+
* `IdentityToolkit` is configured they throw.
|
|
47
|
+
*
|
|
48
|
+
* @see {@link FirebaseVerifier} for the abstract boundary this implements.
|
|
49
|
+
*/
|
|
23
50
|
export class JoseFirebaseVerifier implements FirebaseVerifier {
|
|
51
|
+
/**
|
|
52
|
+
* Create a verifier.
|
|
53
|
+
*
|
|
54
|
+
* @param opts - Verifier configuration.
|
|
55
|
+
* @param opts.projectId - The Firebase project id, used as both the expected token issuer
|
|
56
|
+
* (`https://securetoken.google.com/<projectId>`) and audience.
|
|
57
|
+
* @param opts.keyResolver - The RS256 verification key or a dynamic key resolver function.
|
|
58
|
+
* @param opts.identity - Optional Identity Toolkit client enabling `getUser` / `deleteUser`.
|
|
59
|
+
* @param opts.now - Optional clock returning the current time in seconds; injectable for
|
|
60
|
+
* deterministic tests. Defaults to the system clock.
|
|
61
|
+
*/
|
|
24
62
|
constructor(
|
|
25
63
|
private readonly opts: {
|
|
26
64
|
projectId: string;
|
|
@@ -30,6 +68,18 @@ export class JoseFirebaseVerifier implements FirebaseVerifier {
|
|
|
30
68
|
},
|
|
31
69
|
) {}
|
|
32
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Verify a Firebase ID token and return its decoded payload.
|
|
73
|
+
*
|
|
74
|
+
* Checks the RS256 signature against the configured key, enforces the expected issuer and
|
|
75
|
+
* audience (the project id), and applies the admin SDK's extra checks: a non-empty string
|
|
76
|
+
* subject of at most 128 characters and an `auth_time` that is a number not in the future.
|
|
77
|
+
*
|
|
78
|
+
* @param idToken - The raw Firebase ID token (JWT) to verify.
|
|
79
|
+
* @returns The decoded payload, with `uid` set from `sub` and `email` lifted to a top-level field.
|
|
80
|
+
* @throws If the signature, issuer, audience, or expiry are invalid, if the subject is
|
|
81
|
+
* missing/non-string/too long, or if `auth_time` is missing or in the future.
|
|
82
|
+
*/
|
|
33
83
|
async verifyIdToken(idToken: string): Promise<DecodedIdToken> {
|
|
34
84
|
const options = {
|
|
35
85
|
issuer: `https://securetoken.google.com/${this.opts.projectId}`,
|
|
@@ -51,6 +101,13 @@ export class JoseFirebaseVerifier implements FirebaseVerifier {
|
|
|
51
101
|
return { ...payload, uid: payload.sub, email: payload.email as string | undefined };
|
|
52
102
|
}
|
|
53
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Look up a user record by uid via the Identity Toolkit REST API.
|
|
106
|
+
*
|
|
107
|
+
* @param uid - The user's unique id.
|
|
108
|
+
* @returns The user's `uid` and optional `email`, or `null` when the user does not exist.
|
|
109
|
+
* @throws If no Identity Toolkit client was configured on this verifier.
|
|
110
|
+
*/
|
|
54
111
|
async getUser(uid: string): Promise<{ uid: string; email?: string } | null> {
|
|
55
112
|
if (!this.opts.identity) {
|
|
56
113
|
throw new Error('Identity Toolkit not configured');
|
|
@@ -58,6 +115,13 @@ export class JoseFirebaseVerifier implements FirebaseVerifier {
|
|
|
58
115
|
return this.opts.identity.lookup(uid, this.nowSeconds());
|
|
59
116
|
}
|
|
60
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Delete a user by uid via the Identity Toolkit REST API.
|
|
120
|
+
*
|
|
121
|
+
* @param uid - The user's unique id.
|
|
122
|
+
* @returns A promise that resolves once the user has been deleted.
|
|
123
|
+
* @throws If no Identity Toolkit client was configured on this verifier, or the deletion fails.
|
|
124
|
+
*/
|
|
61
125
|
async deleteUser(uid: string): Promise<void> {
|
|
62
126
|
if (!this.opts.identity) {
|
|
63
127
|
throw new Error('Identity Toolkit not configured');
|
|
@@ -65,6 +129,12 @@ export class JoseFirebaseVerifier implements FirebaseVerifier {
|
|
|
65
129
|
await this.opts.identity.remove(uid, this.nowSeconds());
|
|
66
130
|
}
|
|
67
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Return the current time in seconds, using the injected clock when provided.
|
|
134
|
+
*
|
|
135
|
+
* @returns The current Unix time in seconds.
|
|
136
|
+
* @internal
|
|
137
|
+
*/
|
|
68
138
|
private nowSeconds(): number {
|
|
69
139
|
return this.opts.now ? this.opts.now() : Math.floor(Date.now() / 1000);
|
|
70
140
|
}
|
|
@@ -1,19 +1,46 @@
|
|
|
1
1
|
import { createRemoteJWKSet } from 'jose';
|
|
2
|
-
import { IdentityToolkit } from './identity-toolkit';
|
|
3
|
-
import type { ServiceAccount } from './identity-toolkit';
|
|
4
|
-
import { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './jose-firebase-verifier';
|
|
2
|
+
import { IdentityToolkit } from './identity-toolkit.js';
|
|
3
|
+
import type { ServiceAccount } from './identity-toolkit.js';
|
|
4
|
+
import { JoseFirebaseVerifier, SECURETOKEN_JWK_URL } from './jose-firebase-verifier.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* `JoseFirebaseVerifier` を返す。トークン検証のみ用途(getUser/deleteUser は不要 = Identity Toolkit 無し)。
|
|
7
|
+
* Shared remote JWKS for Google's securetoken keys.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The JWKS URL is fixed, so the set is created once per isolate and shared across verifiers
|
|
11
|
+
* (`jose` caches the fetched keys internally). Lazily initialised on first use.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
13
14
|
*/
|
|
14
15
|
let jwks: ReturnType<typeof createRemoteJWKSet> | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Per-`projectId` cache of token-only verifiers, memoized for the lifetime of the isolate.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
15
21
|
const verifiers = new Map<string, JoseFirebaseVerifier>();
|
|
16
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Create a token-verification-only Firebase verifier for the given project.
|
|
25
|
+
*
|
|
26
|
+
* Uses `createRemoteJWKSet` to fetch Google's securetoken public keys and returns a
|
|
27
|
+
* {@link JoseFirebaseVerifier}. This factory is for token verification only; it configures no
|
|
28
|
+
* Identity Toolkit client, so `getUser` / `deleteUser` are unavailable.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* The remote JWKS is created once per isolate and shared, and the returned verifier is
|
|
32
|
+
* memoized per `projectId`. This preserves the caching behaviour of a module-level JWKS so
|
|
33
|
+
* repeated calls do not re-fetch keys or allocate new verifiers.
|
|
34
|
+
*
|
|
35
|
+
* @param projectId - The Firebase project id whose tokens will be verified.
|
|
36
|
+
* @returns A verifier that validates ID tokens for `projectId`.
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const verifier = createRemoteFirebaseVerifier('my-firebase-project');
|
|
40
|
+
* const decoded = await verifier.verifyIdToken(idToken);
|
|
41
|
+
* console.log(decoded.uid);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
17
44
|
export function createRemoteFirebaseVerifier(projectId: string): JoseFirebaseVerifier {
|
|
18
45
|
jwks ??= createRemoteJWKSet(new URL(SECURETOKEN_JWK_URL));
|
|
19
46
|
let verifier = verifiers.get(projectId);
|
|
@@ -24,13 +51,35 @@ export function createRemoteFirebaseVerifier(projectId: string): JoseFirebaseVer
|
|
|
24
51
|
return verifier;
|
|
25
52
|
}
|
|
26
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Single-entry cache of the service-account verifier, keyed by the raw service-account JSON.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
27
59
|
let saVerifierCache: { key: string; verifier: JoseFirebaseVerifier } | null = null;
|
|
28
60
|
|
|
29
61
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
62
|
+
* Create a Firebase verifier from a service-account JSON string.
|
|
63
|
+
*
|
|
64
|
+
* Unlike {@link createRemoteFirebaseVerifier}, the returned {@link JoseFirebaseVerifier}
|
|
65
|
+
* embeds an {@link IdentityToolkit} client, enabling `getUser` and `deleteUser` in addition to
|
|
66
|
+
* token verification.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* The verifier is cached for the lifetime of the isolate, keyed by the service-account JSON
|
|
70
|
+
* string, and is only rebuilt when that secret changes. The remote JWKS is shared with
|
|
71
|
+
* {@link createRemoteFirebaseVerifier}.
|
|
72
|
+
*
|
|
73
|
+
* @param serviceAccountJson - The service-account key as a JSON string (parsed into {@link ServiceAccount}).
|
|
74
|
+
* @returns A verifier that validates ID tokens and can look up or delete users.
|
|
75
|
+
* @throws If `serviceAccountJson` is not valid JSON.
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* const verifier = createServiceAccountVerifier(env.FIREBASE_SERVICE_ACCOUNT);
|
|
79
|
+
* const decoded = await verifier.verifyIdToken(idToken);
|
|
80
|
+
* const user = await verifier.getUser(decoded.uid);
|
|
81
|
+
* await verifier.deleteUser(decoded.uid);
|
|
82
|
+
* ```
|
|
34
83
|
*/
|
|
35
84
|
export function createServiceAccountVerifier(serviceAccountJson: string): JoseFirebaseVerifier {
|
|
36
85
|
if (saVerifierCache?.key !== serviceAccountJson) {
|
package/src/http/app-env.ts
CHANGED
|
@@ -1,20 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolved runtime environment of the Worker.
|
|
3
|
+
*/
|
|
1
4
|
export type AppEnv = 'development' | 'production';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
7
|
+
* Resolve the runtime environment (development or production) from the Worker `env` binding.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Cloudflare Workers have no filesystem or `child_process` at runtime, so the environment cannot be
|
|
11
|
+
* inferred from the presence of a `.git` directory the way a Node/NestJS process might. Instead, the
|
|
12
|
+
* development signal is carried in the committed launch command: `wrangler dev --var APP_ENV:development`
|
|
13
|
+
* injects `APP_ENV`, while `wrangler deploy` injects nothing. Only `env.APP_ENV === 'development'`
|
|
14
|
+
* resolves to `'development'`; every other case (including a missing binding, i.e. a production deploy)
|
|
15
|
+
* resolves to `'production'`. This "absence defaults to production" semantic keeps the safe side as the
|
|
16
|
+
* default.
|
|
17
|
+
*
|
|
18
|
+
* Because it reads from `env` rather than a request header, it works in both `fetch` and `scheduled`
|
|
19
|
+
* contexts and cannot be spoofed by an incoming request.
|
|
5
20
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* **コミット済みの起動コマンド**に置く: `wrangler dev --var APP_ENV:development`(`deploy` は無注入)。
|
|
9
|
-
* よって `env.APP_ENV === 'development'` の時だけ development、それ以外(注入無し=本番デプロイ)は
|
|
10
|
-
* production に倒す。これは `/api` の「absence/catch = 本番(安全側)」と同じ意味論。
|
|
21
|
+
* @param env - The Worker environment binding, or `null`/`undefined` when unavailable.
|
|
22
|
+
* @returns `'development'` only when `env.APP_ENV` is exactly `'development'`; otherwise `'production'`.
|
|
11
23
|
*
|
|
12
|
-
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* export default {
|
|
27
|
+
* fetch(req, env) {
|
|
28
|
+
* if (resolveAppEnv(env) === 'development') {
|
|
29
|
+
* // enable verbose logging
|
|
30
|
+
* }
|
|
31
|
+
* },
|
|
32
|
+
* };
|
|
33
|
+
* ```
|
|
13
34
|
*/
|
|
14
35
|
export function resolveAppEnv(env: { APP_ENV?: string } | null | undefined): AppEnv {
|
|
15
36
|
return env?.APP_ENV === 'development' ? 'development' : 'production';
|
|
16
37
|
}
|
|
17
38
|
|
|
18
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Shorthand for checking whether the resolved environment is production.
|
|
41
|
+
*
|
|
42
|
+
* @param env - The Worker environment binding, or `null`/`undefined` when unavailable.
|
|
43
|
+
* @returns `true` when {@link resolveAppEnv} resolves to `'production'`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* if (isProductionEnv(env)) {
|
|
48
|
+
* // skip dev-only diagnostics
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
19
52
|
export const isProductionEnv = (env: { APP_ENV?: string } | null | undefined): boolean =>
|
|
20
53
|
resolveAppEnv(env) === 'production';
|
package/src/http/app-info.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Client application metadata derived from the `x-amz-meta-*` request headers.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Reproduces the per-request app identity that a NestJS service would expose so a Hono app
|
|
8
|
+
* can read the same client-supplied version/uuid pair without changing the wire contract.
|
|
9
|
+
*/
|
|
4
10
|
export interface AppInfo {
|
|
11
|
+
/** Client application version from `x-amz-meta-version`, or `null` when the header is absent. */
|
|
5
12
|
version: string | null;
|
|
13
|
+
/** Client installation identifier from `x-amz-meta-uuid`, or `null` when the header is absent. */
|
|
6
14
|
uuid: string | null;
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
|
-
* `x-amz-meta-version` / `x-amz-meta-uuid`
|
|
11
|
-
*
|
|
18
|
+
* Read {@link AppInfo} from the `x-amz-meta-version` / `x-amz-meta-uuid` request headers.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Typically called by an auth middleware that stores the result per request via
|
|
22
|
+
* `c.set('appInfo', getAppInfo(c))`. Missing headers resolve to `null` rather than throwing.
|
|
23
|
+
*
|
|
24
|
+
* @param c - The Hono request context to read headers from.
|
|
25
|
+
* @returns The client application metadata for the current request.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* app.use(async (c, next) => {
|
|
30
|
+
* c.set('appInfo', getAppInfo(c));
|
|
31
|
+
* await next();
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
12
34
|
*/
|
|
13
35
|
export const getAppInfo = (c: Context): AppInfo => ({
|
|
14
36
|
version: c.req.header('x-amz-meta-version') ?? null,
|
package/src/http/http-status.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* HTTP status codes mirroring the `HttpStatus` enum from `@nestjs/common`.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Provides a single source of truth for referencing status codes by the same names NestJS uses, so a
|
|
6
|
+
* Hono app can emit responses whose status matches a NestJS service byte-for-byte. The member set and
|
|
7
|
+
* numeric values intentionally track `@nestjs/common` rather than the IANA registry, including a few
|
|
8
|
+
* non-standard codes that NestJS ships.
|
|
5
9
|
*/
|
|
6
10
|
export enum HttpStatus {
|
|
7
11
|
CONTINUE = 100,
|
|
@@ -17,7 +21,9 @@ export enum HttpStatus {
|
|
|
17
21
|
PARTIAL_CONTENT = 206,
|
|
18
22
|
MULTI_STATUS = 207,
|
|
19
23
|
ALREADY_REPORTED = 208,
|
|
24
|
+
/** Non-standard WebDAV extension carried over from NestJS. */
|
|
20
25
|
CONTENT_DIFFERENT = 210,
|
|
26
|
+
/** Multiple Choices (300). Named `AMBIGUOUS` to match NestJS. */
|
|
21
27
|
AMBIGUOUS = 300,
|
|
22
28
|
MOVED_PERMANENTLY = 301,
|
|
23
29
|
FOUND = 302,
|
|
@@ -43,13 +49,16 @@ export enum HttpStatus {
|
|
|
43
49
|
UNSUPPORTED_MEDIA_TYPE = 415,
|
|
44
50
|
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
|
|
45
51
|
EXPECTATION_FAILED = 417,
|
|
52
|
+
/** "I'm a teapot" (418), from RFC 2324. */
|
|
46
53
|
I_AM_A_TEAPOT = 418,
|
|
54
|
+
/** Misdirected Request (421). Named `MISDIRECTED` to match NestJS. */
|
|
47
55
|
MISDIRECTED = 421,
|
|
48
56
|
UNPROCESSABLE_ENTITY = 422,
|
|
49
57
|
LOCKED = 423,
|
|
50
58
|
FAILED_DEPENDENCY = 424,
|
|
51
59
|
PRECONDITION_REQUIRED = 428,
|
|
52
60
|
TOO_MANY_REQUESTS = 429,
|
|
61
|
+
/** Non-standard code carried over from NestJS. */
|
|
53
62
|
UNRECOVERABLE_ERROR = 456,
|
|
54
63
|
INTERNAL_SERVER_ERROR = 500,
|
|
55
64
|
NOT_IMPLEMENTED = 501,
|