@dereekb/firebase-server 13.1.0 → 13.2.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/index.cjs.js CHANGED
@@ -7,6 +7,7 @@ var date = require('@dereekb/date');
7
7
  var makeError = require('make-error');
8
8
  var rxjs = require('rxjs');
9
9
  var firestore = require('@google-cloud/firestore');
10
+ var crypto = require('crypto');
10
11
  var common = require('@nestjs/common');
11
12
  var nestjs = require('@dereekb/nestjs');
12
13
  var v2 = require('firebase-functions/v2');
@@ -1048,6 +1049,140 @@ function googleCloudFirestoreDrivers() {
1048
1049
  */
1049
1050
  const googleCloudFirestoreContextFactory = firebase.firestoreContextFactory(googleCloudFirestoreDrivers());
1050
1051
 
1052
+ // MARK: Encrypted Field
1053
+ /**
1054
+ * AES-256-GCM encryption constants.
1055
+ */
1056
+ const ENCRYPTED_FIELD_ALGORITHM = 'aes-256-gcm';
1057
+ const ENCRYPTED_FIELD_IV_LENGTH = 12;
1058
+ const ENCRYPTED_FIELD_AUTH_TAG_LENGTH = 16;
1059
+ const ENCRYPTED_FIELD_KEY_LENGTH = 32;
1060
+ /**
1061
+ * Resolves the encryption key Buffer from a secret source.
1062
+ *
1063
+ * @param source - The secret source configuration.
1064
+ * @returns A 32-byte Buffer for AES-256 encryption.
1065
+ * @throws Error if the resolved key is not 64 hex characters.
1066
+ */
1067
+ function resolveEncryptionKey(source) {
1068
+ let hex;
1069
+ if (typeof source === 'string') {
1070
+ hex = source;
1071
+ }
1072
+ else if (typeof source === 'function') {
1073
+ hex = source();
1074
+ }
1075
+ else {
1076
+ const envValue = process.env[source.env];
1077
+ if (!envValue) {
1078
+ throw new Error(`firestoreEncryptedField: environment variable "${source.env}" is not set.`);
1079
+ }
1080
+ hex = envValue;
1081
+ }
1082
+ if (hex.length !== ENCRYPTED_FIELD_KEY_LENGTH * 2) {
1083
+ throw new Error(`firestoreEncryptedField: expected a ${ENCRYPTED_FIELD_KEY_LENGTH * 2}-character hex key, got ${hex.length} characters.`);
1084
+ }
1085
+ return Buffer.from(hex, 'hex');
1086
+ }
1087
+ /**
1088
+ * Encrypts a JSON-serializable value to a base64-encoded string.
1089
+ *
1090
+ * Format: base64(IV (12 bytes) + ciphertext + authTag (16 bytes))
1091
+ *
1092
+ * @param value - The value to encrypt.
1093
+ * @param key - The 32-byte encryption key.
1094
+ * @returns The encrypted value as a base64 string.
1095
+ */
1096
+ function encryptValue(value, key) {
1097
+ const iv = crypto.randomBytes(ENCRYPTED_FIELD_IV_LENGTH);
1098
+ const cipher = crypto.createCipheriv(ENCRYPTED_FIELD_ALGORITHM, key, iv);
1099
+ const plaintext = JSON.stringify(value);
1100
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
1101
+ const authTag = cipher.getAuthTag();
1102
+ const combined = Buffer.concat([iv, encrypted, authTag]);
1103
+ return combined.toString('base64');
1104
+ }
1105
+ /**
1106
+ * Decrypts a base64-encoded string back to the original value.
1107
+ *
1108
+ * @param encoded - The base64-encoded encrypted string (IV + ciphertext + authTag).
1109
+ * @param key - The 32-byte encryption key.
1110
+ * @returns The decrypted value.
1111
+ */
1112
+ function decryptValue(encoded, key) {
1113
+ const combined = Buffer.from(encoded, 'base64');
1114
+ const iv = combined.subarray(0, ENCRYPTED_FIELD_IV_LENGTH);
1115
+ const authTag = combined.subarray(combined.length - ENCRYPTED_FIELD_AUTH_TAG_LENGTH);
1116
+ const ciphertext = combined.subarray(ENCRYPTED_FIELD_IV_LENGTH, combined.length - ENCRYPTED_FIELD_AUTH_TAG_LENGTH);
1117
+ const decipher = crypto.createDecipheriv(ENCRYPTED_FIELD_ALGORITHM, key, iv);
1118
+ decipher.setAuthTag(authTag);
1119
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1120
+ return JSON.parse(decrypted.toString('utf8'));
1121
+ }
1122
+ /**
1123
+ * Creates a Firestore field mapping that encrypts/decrypts a JSON-serializable value
1124
+ * using AES-256-GCM. The value is stored in Firestore as a base64-encoded string.
1125
+ *
1126
+ * The encryption key is resolved from the configured secret source on each read/write,
1127
+ * allowing for key rotation via environment variable changes.
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * const jwksField = firestoreEncryptedField<JWKSet>({
1132
+ * secret: { env: 'FIRESTORE_ENCRYPTION_KEY' },
1133
+ * default: () => ({ keys: [] })
1134
+ * });
1135
+ * ```
1136
+ *
1137
+ * @template T - The JSON-serializable value type.
1138
+ * @param config - Encryption field configuration.
1139
+ * @returns A field mapping configuration for encrypted values.
1140
+ */
1141
+ function firestoreEncryptedField(config) {
1142
+ const { secret, default: defaultValue } = config;
1143
+ return firebase.firestoreField({
1144
+ default: defaultValue,
1145
+ fromData: (data) => {
1146
+ const key = resolveEncryptionKey(secret);
1147
+ return decryptValue(data, key);
1148
+ },
1149
+ toData: (value) => {
1150
+ const key = resolveEncryptionKey(secret);
1151
+ return encryptValue(value, key);
1152
+ }
1153
+ });
1154
+ }
1155
+ /**
1156
+ * Creates a Firestore field mapping for an optional encrypted field.
1157
+ *
1158
+ * When the value is null/undefined, it is stored/read as null. When present, it is
1159
+ * encrypted/decrypted using AES-256-GCM.
1160
+ *
1161
+ * @example
1162
+ * ```typescript
1163
+ * const optionalSecretField = optionalFirestoreEncryptedField<OAuthClientSecret>({
1164
+ * secret: { env: 'FIRESTORE_ENCRYPTION_KEY' }
1165
+ * });
1166
+ * ```
1167
+ *
1168
+ * @template T - The JSON-serializable value type.
1169
+ * @param config - Encryption field configuration.
1170
+ * @returns A field mapping configuration for optional encrypted values.
1171
+ */
1172
+ function optionalFirestoreEncryptedField(config) {
1173
+ const { secret } = config;
1174
+ return firebase.optionalFirestoreField({
1175
+ transformFromData: (data) => {
1176
+ const key = resolveEncryptionKey(secret);
1177
+ return decryptValue(data, key);
1178
+ },
1179
+ transformToData: (value) => {
1180
+ const key = resolveEncryptionKey(secret);
1181
+ return encryptValue(value, key);
1182
+ }
1183
+ });
1184
+ }
1185
+
1051
1186
  function assertContextHasAuth(context) {
1052
1187
  if (!isContextWithAuthData(context)) {
1053
1188
  throw unauthenticatedContextHasNoUidError();
@@ -2766,6 +2901,7 @@ exports.firebaseServerStorageModuleMetadata = firebaseServerStorageModuleMetadat
2766
2901
  exports.firebaseServerValidationError = firebaseServerValidationError;
2767
2902
  exports.firebaseServerValidationServerError = firebaseServerValidationServerError;
2768
2903
  exports.firestoreClientQueryConstraintFunctionsDriver = firestoreClientQueryConstraintFunctionsDriver;
2904
+ exports.firestoreEncryptedField = firestoreEncryptedField;
2769
2905
  exports.firestoreServerIncrementUpdateToUpdateData = firestoreServerIncrementUpdateToUpdateData;
2770
2906
  exports.forbiddenError = forbiddenError;
2771
2907
  exports.getAuthUserOrUndefined = getAuthUserOrUndefined;
@@ -2822,6 +2958,7 @@ exports.onCallUpdateModel = onCallUpdateModel;
2822
2958
  exports.onScheduleHandlerWithNestApplicationFactory = onScheduleHandlerWithNestApplicationFactory;
2823
2959
  exports.onScheduleHandlerWithNestContextFactory = onScheduleHandlerWithNestContextFactory;
2824
2960
  exports.optionalAuthContext = optionalAuthContext;
2961
+ exports.optionalFirestoreEncryptedField = optionalFirestoreEncryptedField;
2825
2962
  exports.permissionDeniedError = permissionDeniedError;
2826
2963
  exports.phoneNumberAlreadyExistsError = phoneNumberAlreadyExistsError;
2827
2964
  exports.preconditionConflictError = preconditionConflictError;