@dereekb/firebase-server 13.0.7 → 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();
@@ -1153,10 +1288,6 @@ function __param(paramIndex, decorator) {
1153
1288
  return function (target, key) { decorator(target, key, paramIndex); }
1154
1289
  }
1155
1290
 
1156
- function __metadata(metadataKey, metadataValue) {
1157
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
1158
- }
1159
-
1160
1291
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
1161
1292
  var e = new Error(message);
1162
1293
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
@@ -1734,43 +1865,34 @@ function firebaseServerActionsTransformContext(options) {
1734
1865
  }
1735
1866
  const FIREBASE_SERVER_VALIDATION_ERROR_CODE = 'VALIDATION_ERROR';
1736
1867
  /**
1737
- *
1738
- * @param validationError
1739
- * @returns
1868
+ * Creates a server error object from ArkType validation errors.
1740
1869
  */
1741
- function firebaseServerValidationServerError(validationError) {
1742
- const nestValidationExceptionFactory = new common.ValidationPipe({
1743
- forbidUnknownValues: false
1744
- }).createExceptionFactory();
1745
- const nestError = nestValidationExceptionFactory(validationError);
1746
- const data = nestError.getResponse();
1870
+ function firebaseServerValidationServerError(validationErrors) {
1747
1871
  return {
1748
1872
  message: 'One or more data/form validation errors occurred.',
1749
1873
  code: FIREBASE_SERVER_VALIDATION_ERROR_CODE,
1750
- data
1874
+ data: {
1875
+ message: validationErrors.summary
1876
+ }
1751
1877
  };
1752
1878
  }
1753
1879
  /**
1754
1880
  * Creates a new badRequestError with the validation error details as the response data.
1755
- *
1756
- * @param validationError
1757
- * @returns
1758
1881
  */
1759
- function firebaseServerValidationError(validationError) {
1760
- const serverError = firebaseServerValidationServerError(validationError);
1882
+ function firebaseServerValidationError(validationErrors) {
1883
+ const serverError = firebaseServerValidationServerError(validationErrors);
1761
1884
  return badRequestError(serverError);
1762
1885
  }
1763
1886
  function firebaseServerActionsTransformFactory(options) {
1764
- const { logError, defaultValidationOptions } = options ?? {};
1887
+ const { logError } = options ?? {};
1765
1888
  const logErrorFunction = logError !== false ? (typeof logError === 'function' ? logError : defaultFirebaseServerActionsTransformFactoryLogErrorFunction) : util.mapIdentityFunction;
1766
1889
  return model.transformAndValidateObjectFactory({
1767
- handleValidationError: (validationError) => {
1768
- const serverError = firebaseServerValidationServerError(validationError);
1890
+ handleValidationError: async (validationErrors) => {
1891
+ const serverError = firebaseServerValidationServerError(validationErrors);
1769
1892
  const { data } = serverError;
1770
1893
  logErrorFunction(data);
1771
1894
  throw badRequestError(serverError);
1772
- },
1773
- defaultValidationOptions
1895
+ }
1774
1896
  });
1775
1897
  }
1776
1898
 
@@ -1829,8 +1951,7 @@ exports.FirebaseAppCheckMiddleware = class FirebaseAppCheckMiddleware {
1829
1951
  exports.FirebaseAppCheckMiddleware = __decorate([
1830
1952
  common.Injectable(),
1831
1953
  __param(0, common.Optional()),
1832
- __param(0, common.Inject(GlobalRoutePrefixConfig)),
1833
- __metadata("design:paramtypes", [Object])
1954
+ __param(0, common.Inject(GlobalRoutePrefixConfig))
1834
1955
  ], exports.FirebaseAppCheckMiddleware);
1835
1956
  /**
1836
1957
  * Verifies the AppCheck parameter. If it fails, a value is returned.
@@ -2780,6 +2901,7 @@ exports.firebaseServerStorageModuleMetadata = firebaseServerStorageModuleMetadat
2780
2901
  exports.firebaseServerValidationError = firebaseServerValidationError;
2781
2902
  exports.firebaseServerValidationServerError = firebaseServerValidationServerError;
2782
2903
  exports.firestoreClientQueryConstraintFunctionsDriver = firestoreClientQueryConstraintFunctionsDriver;
2904
+ exports.firestoreEncryptedField = firestoreEncryptedField;
2783
2905
  exports.firestoreServerIncrementUpdateToUpdateData = firestoreServerIncrementUpdateToUpdateData;
2784
2906
  exports.forbiddenError = forbiddenError;
2785
2907
  exports.getAuthUserOrUndefined = getAuthUserOrUndefined;
@@ -2836,6 +2958,7 @@ exports.onCallUpdateModel = onCallUpdateModel;
2836
2958
  exports.onScheduleHandlerWithNestApplicationFactory = onScheduleHandlerWithNestApplicationFactory;
2837
2959
  exports.onScheduleHandlerWithNestContextFactory = onScheduleHandlerWithNestContextFactory;
2838
2960
  exports.optionalAuthContext = optionalAuthContext;
2961
+ exports.optionalFirestoreEncryptedField = optionalFirestoreEncryptedField;
2839
2962
  exports.permissionDeniedError = permissionDeniedError;
2840
2963
  exports.phoneNumberAlreadyExistsError = phoneNumberAlreadyExistsError;
2841
2964
  exports.preconditionConflictError = preconditionConflictError;