@dereekb/firebase-server 13.33.0 → 13.34.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/zoho/index.cjs.js CHANGED
@@ -1,57 +1,114 @@
1
1
  'use strict';
2
2
 
3
3
  var firebase = require('@dereekb/firebase');
4
+ var firebaseServer = require('@dereekb/firebase-server');
4
5
  var util = require('@dereekb/util');
6
+ var nestjs = require('@dereekb/nestjs');
5
7
  var zoho = require('@dereekb/zoho');
6
8
  var model = require('@dereekb/firebase-server/model');
7
9
  var date = require('@dereekb/date');
8
10
  var common = require('@nestjs/common');
9
- var nestjs = require('@dereekb/zoho/nestjs');
11
+ var nestjs$1 = require('@dereekb/zoho/nestjs');
10
12
  var config = require('@nestjs/config');
11
- var firebaseServer = require('@dereekb/firebase-server');
12
13
 
13
14
  /**
14
15
  * {@link SystemState} type identifier for storing Zoho access tokens in Firestore.
15
16
  */ var ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE = 'zoho_access_token';
16
- var zohoAccessTokenSystemStateEmbeddedTokenConverter = firebase.firestoreSubObject({
17
- objectField: {
18
- fields: {
19
- key: firebase.firestoreString(),
20
- accessToken: firebase.firestoreString(),
21
- scope: firebase.firestoreString(),
22
- apiDomain: firebase.firestoreString(),
23
- expiresIn: firebase.firestoreNumber({
24
- default: 3600
25
- }),
26
- expiresAt: firebase.firestoreDate()
17
+ /**
18
+ * Creates the embedded-token converter, encrypting the `accessToken` at rest.
19
+ *
20
+ * This is a factory rather than a module-level const because `firestoreEncryptedField` resolves and
21
+ * validates the encryption key eagerly at construction — the secret must be known at runtime.
22
+ *
23
+ * ONLY `accessToken` is encrypted, deliberately. `firestoreEncryptedField` round-trips through
24
+ * `JSON.stringify`/`JSON.parse`, so anything placed inside the encrypted blob loses its type — and
25
+ * `expiresAt` is a `Date`. Encrypting the token string alone keeps every date outside the blob,
26
+ * which is what lets {@link zohoAccessTokenSystemStateDataConverterFactory} keep filtering expired
27
+ * entries without having to decrypt them first. Do not "improve" this by encrypting the whole
28
+ * token object or the `tokens` array.
29
+ *
30
+ * Accepted trade-off: `key`, `scope`, `apiDomain`, `expiresIn` and `expiresAt` remain plaintext at
31
+ * rest. None of them is a credential.
32
+ *
33
+ * @param config - The encryption configuration.
34
+ * @returns The embedded token field converter.
35
+ */ function zohoAccessTokenSystemStateEmbeddedTokenConverterFactory(config) {
36
+ return firebase.firestoreSubObject({
37
+ objectField: {
38
+ fields: {
39
+ key: firebase.firestoreString(),
40
+ accessToken: firebaseServer.firestoreEncryptedField({
41
+ secret: config.encryptionSecret,
42
+ default: '',
43
+ // This is a cache of ~1h tokens, so an undecryptable entry is a cache MISS, not an error.
44
+ // The empty sentinel is dropped by the `tokens` filter, and the next Zoho call re-mints.
45
+ // This is also what makes a rotated secret survivable here (unlike uecp/jwks).
46
+ onDecodeFailure: function onDecodeFailure() {
47
+ return '';
48
+ }
49
+ }),
50
+ scope: firebase.firestoreString(),
51
+ apiDomain: firebase.firestoreString(),
52
+ expiresIn: firebase.firestoreNumber({
53
+ default: 3600
54
+ }),
55
+ expiresAt: firebase.firestoreDate()
56
+ }
27
57
  }
28
- }
29
- });
58
+ });
59
+ }
30
60
  /**
31
61
  * Firestore field converter for {@link ZohoAccessTokenSystemStateData}.
32
62
  *
33
63
  * Automatically filters out expired tokens on read and enforces uniqueness by service key.
34
64
  * Must be registered in the app's {@link SystemStateStoredDataConverterMap} under
35
65
  * the {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} key.
36
- */ var zohoAccessTokenSystemStateDataConverter = firebase.firestoreSubObject({
37
- objectField: {
38
- fields: {
39
- tokens: firebase.firestoreObjectArray({
40
- firestoreField: zohoAccessTokenSystemStateEmbeddedTokenConverter,
41
- filterUnique: util.filterUniqueFunction(function(x) {
42
- return x.key;
66
+ */ /**
67
+ * Builds the {@link ZohoAccessTokenSystemStateData} converter around a given embedded-token converter.
68
+ *
69
+ * Shared by the encrypted factory and the deprecated plaintext const so the array's expiry filter and
70
+ * per-key dedup behavior can only ever be defined once.
71
+ *
72
+ * @param embeddedTokenConverter - The converter for each entry in the `tokens` array.
73
+ * @returns The stored-data field converter.
74
+ */ function zohoAccessTokenSystemStateDataConverterForEmbeddedTokenConverter(embeddedTokenConverter) {
75
+ return firebase.firestoreSubObject({
76
+ objectField: {
77
+ fields: {
78
+ tokens: firebase.firestoreObjectArray({
79
+ firestoreField: embeddedTokenConverter,
80
+ filterUnique: util.filterUniqueFunction(function(x) {
81
+ return x.key;
82
+ }),
83
+ // `firestoreObjectArray` maps BEFORE it filters, so this runs on already-decoded entries.
84
+ // The `accessToken` check is what drops an entry whose decryption failed (onDecodeFailure
85
+ // leaves an empty string behind) — without it such an entry would surface as a token with
86
+ // an empty secret rather than as a cache miss.
87
+ filter: function filter(x) {
88
+ return Boolean(x === null || x === void 0 ? void 0 : x.accessToken) && ((x === null || x === void 0 ? void 0 : x.expiresAt) ? !util.isPast(x.expiresAt) : true // filter out empty/expired values
89
+ );
90
+ }
43
91
  }),
44
- filter: function filter(x) {
45
- return (x === null || x === void 0 ? void 0 : x.expiresAt) ? !util.isPast(x.expiresAt) : true // filter out expired values or values that have no expiration
46
- ;
47
- }
48
- }),
49
- lat: firebase.firestoreDate({
50
- saveDefaultAsNow: true
51
- })
92
+ lat: firebase.firestoreDate({
93
+ saveDefaultAsNow: true
94
+ })
95
+ }
52
96
  }
53
- }
54
- });
97
+ });
98
+ }
99
+ /**
100
+ * Creates the {@link ZohoAccessTokenSystemStateData} converter, encrypting each token's
101
+ * `accessToken` at rest.
102
+ *
103
+ * Register the result in a SERVER-ONLY converter map under {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} —
104
+ * see `systemStatePrivateFirestoreCollection()` in `@dereekb/firebase-server/model`. It must never be
105
+ * registered in an app's client-shared `SystemStateStoredDataConverterMap`.
106
+ *
107
+ * @param config - The encryption configuration.
108
+ * @returns The stored-data field converter.
109
+ */ function zohoAccessTokenSystemStateDataConverterFactory(config) {
110
+ return zohoAccessTokenSystemStateDataConverterForEmbeddedTokenConverter(zohoAccessTokenSystemStateEmbeddedTokenConverterFactory(config));
111
+ }
55
112
  /**
56
113
  * Loads the {@link SystemStateDocument} that stores {@link ZohoAccessTokenSystemStateData},
57
114
  * using {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} as the document ID.
@@ -67,6 +124,28 @@ var zohoAccessTokenSystemStateEmbeddedTokenConverter = firebase.firestoreSubObje
67
124
  */ function loadZohoAccessTokenSystemState(accessor) {
68
125
  return accessor.loadDocumentForId(ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE);
69
126
  }
127
+ // COMPAT: Deprecated aliases
128
+ /**
129
+ * @deprecated stores the access token in PLAINTEXT. Use
130
+ * {@link zohoAccessTokenSystemStateEmbeddedTokenConverterFactory} instead, which encrypts it at rest.
131
+ */ var zohoAccessTokenSystemStateEmbeddedTokenConverter = firebase.firestoreSubObject({
132
+ objectField: {
133
+ fields: {
134
+ key: firebase.firestoreString(),
135
+ accessToken: firebase.firestoreString(),
136
+ scope: firebase.firestoreString(),
137
+ apiDomain: firebase.firestoreString(),
138
+ expiresIn: firebase.firestoreNumber({
139
+ default: 3600
140
+ }),
141
+ expiresAt: firebase.firestoreDate()
142
+ }
143
+ }
144
+ });
145
+ /**
146
+ * @deprecated stores access tokens in PLAINTEXT. Use {@link zohoAccessTokenSystemStateDataConverterFactory}
147
+ * instead, and register it on a server-only SystemStatePrivate collection.
148
+ */ var zohoAccessTokenSystemStateDataConverter = zohoAccessTokenSystemStateDataConverterForEmbeddedTokenConverter(zohoAccessTokenSystemStateEmbeddedTokenConverter);
70
149
 
71
150
  function _array_like_to_array$1(arr, len) {
72
151
  if (len == null || len > arr.length) len = arr.length;
@@ -105,7 +184,7 @@ function _async_to_generator$2(fn) {
105
184
  });
106
185
  };
107
186
  }
108
- function _define_property$4(obj, key, value) {
187
+ function _define_property$5(obj, key, value) {
109
188
  if (key in obj) {
110
189
  Object.defineProperty(obj, key, {
111
190
  value: value,
@@ -134,7 +213,7 @@ function _object_spread$2(target) {
134
213
  }));
135
214
  }
136
215
  ownKeys.forEach(function(key) {
137
- _define_property$4(target, key, source[key]);
216
+ _define_property$5(target, key, source[key]);
138
217
  });
139
218
  }
140
219
  return target;
@@ -275,12 +354,19 @@ function _ts_generator$2(thisArg, body) {
275
354
  * Tokens are stored in a single {@link SystemState} document (type {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE})
276
355
  * and token updates/clears use Firestore transactions for concurrency safety.
277
356
  *
357
+ * The access token is a credential, so pass a SERVER-ONLY collection — a
358
+ * `systemStatePrivateFirestoreCollection()` from `@dereekb/firebase-server/model`, whose converter
359
+ * map registers {@link zohoAccessTokenSystemStateDataConverterFactory} and encrypts the token at
360
+ * rest. Passing an app's client-shared `systemStateCollection` still type-checks (for backwards
361
+ * compatibility) but requires the converter to be registered in the client-shared map, which drags
362
+ * `@dereekb/firebase-server` into browser builds.
363
+ *
278
364
  * @param systemStateCollection - The Firestore collection for system state documents.
279
365
  * @returns A cache service backed by Firestore system state documents.
280
366
  *
281
367
  * @example
282
368
  * ```ts
283
- * const cacheService = firebaseZohoAccountsAccessTokenCacheService(systemStateCollection);
369
+ * const cacheService = firebaseZohoAccountsAccessTokenCacheService(systemStatePrivateCollection);
284
370
  * const cache = cacheService.loadZohoAccessTokenCache('my-zoho-service');
285
371
  * const token = await cache.loadCachedToken();
286
372
  * ```
@@ -456,6 +542,78 @@ function _ts_generator$2(thisArg, body) {
456
542
  return service;
457
543
  }
458
544
 
545
+ function _define_property$4(obj, key, value) {
546
+ if (key in obj) {
547
+ Object.defineProperty(obj, key, {
548
+ value: value,
549
+ enumerable: true,
550
+ configurable: true,
551
+ writable: true
552
+ });
553
+ } else {
554
+ obj[key] = value;
555
+ }
556
+ return obj;
557
+ }
558
+ // MARK: Environment Variable Keys
559
+ /**
560
+ * Environment variable name for the Zoho access token cache encryption secret
561
+ * (hex-encoded AES-256 key).
562
+ *
563
+ * There is NO key rotation — `firestoreEncryptedField` resolves and validates the key once at
564
+ * converter construction and closes over it. Unlike the `uecp` and `oidcJwksKey` secrets, however,
565
+ * rotating this one is SURVIVABLE: the Zoho converter supplies an `onDecodeFailure` handler, so
566
+ * every entry written under the old key simply degrades to a cache miss and the next Zoho call
567
+ * re-mints a token. Rotation costs one extra token request per service key, not an outage.
568
+ */ var ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY = 'ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET';
569
+ /**
570
+ * Deterministic secret used when running in a testing environment and no real secret is configured,
571
+ * so specs never need a live credential.
572
+ *
573
+ * Deliberately distinct from the OIDC JWKS and UserExternalConnection testing secrets so a leaked
574
+ * emulator blob is attributable. ("Zoho Access Token Cache Test Key", hex-encoded.)
575
+ */ var TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET = '5a6f686f2041636365737320546f6b656e2043616368652054657374204b6579';
576
+ // MARK: Config
577
+ /**
578
+ * Reads the Zoho access token encryption secret from the environment.
579
+ *
580
+ * @param configService - The Nest config service used to read the encryption secret.
581
+ * @param envService - Used to detect a testing environment for the secret fallback.
582
+ * @returns The validated encryption secret.
583
+ * @throws {Error} When the configured secret is invalid outside a testing environment.
584
+ */ function zohoAccessTokenEncryptionSecretFactory(configService, envService) {
585
+ var _configService_get;
586
+ var encryptionSecret = (_configService_get = configService.get(ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY)) !== null && _configService_get !== void 0 ? _configService_get : '';
587
+ if (!nestjs.isValidAES256GCMEncryptionSecret(encryptionSecret)) {
588
+ if (envService.isTestingEnv) {
589
+ encryptionSecret = TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET;
590
+ } else {
591
+ throw new Error("zohoAccessTokenEncryptionSecretFactory: The secret provided by ".concat(ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY, " is not valid. Expected a 64-character hexadecimal string."));
592
+ }
593
+ }
594
+ return encryptionSecret;
595
+ }
596
+ // MARK: Converter Map Entry
597
+ /**
598
+ * Builds the converter map entry for the Zoho access token cache, for use in a SERVER-ONLY
599
+ * SystemState converter map.
600
+ *
601
+ * @param config - The encryption configuration.
602
+ * @returns A partial converter map containing only the Zoho access token entry.
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * const collections = systemStatePrivateFirestoreCollection({
607
+ * firestoreContext,
608
+ * converters: {
609
+ * ...zohoAccessTokenSystemStatePrivateConverterEntry({ encryptionSecret })
610
+ * }
611
+ * });
612
+ * ```
613
+ */ function zohoAccessTokenSystemStatePrivateConverterEntry(config) {
614
+ return _define_property$4({}, ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE, zohoAccessTokenSystemStateDataConverterFactory(config));
615
+ }
616
+
459
617
  function _type_of$3(obj) {
460
618
  "@swc/helpers - typeof";
461
619
  return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj;
@@ -1114,7 +1272,7 @@ exports.ZohoUserExternalConnectionOAuthService = __decorate([
1114
1272
  __param(1, common.Inject(model.UserExternalConnectionStateCoder)),
1115
1273
  __param(2, common.Inject(model.UserExternalConnectionServerActions)),
1116
1274
  __param(3, common.Inject(model.UserExternalConnectionAccessor)),
1117
- __param(4, common.Inject(nestjs.ZohoAccountsOAuthApi))
1275
+ __param(4, common.Inject(nestjs$1.ZohoAccountsOAuthApi))
1118
1276
  ], exports.ZohoUserExternalConnectionOAuthService);
1119
1277
 
1120
1278
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
@@ -1626,6 +1784,8 @@ function _unsupported_iterable_to_array(o, minLen) {
1626
1784
  }
1627
1785
 
1628
1786
  exports.DEFAULT_ZOHO_OAUTH_SCOPES = DEFAULT_ZOHO_OAUTH_SCOPES;
1787
+ exports.TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET = TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET;
1788
+ exports.ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY = ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY;
1629
1789
  exports.ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE = ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE;
1630
1790
  exports.ZOHO_EXTRA_ACCOUNTS_SERVER_KEY = ZOHO_EXTRA_ACCOUNTS_SERVER_KEY;
1631
1791
  exports.ZOHO_EXTRA_API_DOMAIN_KEY = ZOHO_EXTRA_API_DOMAIN_KEY;
@@ -1639,8 +1799,12 @@ exports.appZohoUserExternalConnectionOAuthModuleMetadata = appZohoUserExternalCo
1639
1799
  exports.firebaseZohoAccountsAccessTokenCacheService = firebaseZohoAccountsAccessTokenCacheService;
1640
1800
  exports.loadZohoAccessTokenSystemState = loadZohoAccessTokenSystemState;
1641
1801
  exports.userExternalConnectionZohoAccessTokenCache = userExternalConnectionZohoAccessTokenCache;
1802
+ exports.zohoAccessTokenEncryptionSecretFactory = zohoAccessTokenEncryptionSecretFactory;
1642
1803
  exports.zohoAccessTokenFromUserExternalConnectionCredentials = zohoAccessTokenFromUserExternalConnectionCredentials;
1643
1804
  exports.zohoAccessTokenSystemStateDataConverter = zohoAccessTokenSystemStateDataConverter;
1805
+ exports.zohoAccessTokenSystemStateDataConverterFactory = zohoAccessTokenSystemStateDataConverterFactory;
1644
1806
  exports.zohoAccessTokenSystemStateEmbeddedTokenConverter = zohoAccessTokenSystemStateEmbeddedTokenConverter;
1807
+ exports.zohoAccessTokenSystemStateEmbeddedTokenConverterFactory = zohoAccessTokenSystemStateEmbeddedTokenConverterFactory;
1808
+ exports.zohoAccessTokenSystemStatePrivateConverterEntry = zohoAccessTokenSystemStatePrivateConverterEntry;
1645
1809
  exports.zohoUserExternalConnectionCredentials = zohoUserExternalConnectionCredentials;
1646
1810
  exports.zohoUserExternalConnectionOAuthServiceConfigFactory = zohoUserExternalConnectionOAuthServiceConfigFactory;
package/zoho/index.esm.js CHANGED
@@ -1,55 +1,112 @@
1
1
  import { firestoreSubObject, firestoreDate, firestoreNumber, firestoreString, firestoreObjectArray, ZOHO_USER_EXTERNAL_CONNECTION_PROVIDER_TYPE } from '@dereekb/firebase';
2
+ import { firestoreEncryptedField, FirebaseServerEnvService } from '@dereekb/firebase-server';
2
3
  import { isPast, filterUniqueFunction, MS_IN_SECOND } from '@dereekb/util';
4
+ import { isValidAES256GCMEncryptionSecret } from '@dereekb/nestjs';
3
5
  import { ZOHO_ACCOUNTS_PROFILE_READ_SCOPE, zohoAccountsConfigApiUrl, zohoAccountsAuthorizeUrlFactory, zohoOAuthScopesFromScopeString, zohoAccountsApiUrlKeyForApiUrl, ZOHO_ACCOUNTS_API_URLS, ZOHO_OAUTH_SCOPE_DELIMITER } from '@dereekb/zoho';
4
6
  import { userExternalConnectionOAuthControllerPath, userExternalConnectionOAuthRoutesForGlobalRouteExclude, userExternalConnectionOAuthServiceConfigFactory, UserExternalConnectionOAuthServiceConfig, UserExternalConnectionStateCoder, UserExternalConnectionServerActions, UserExternalConnectionAccessor, AbstractUserExternalConnectionOAuthService, mergeRefreshedUserExternalConnectionCredentials, AbstractUserExternalConnectionOAuthController } from '@dereekb/firebase-server/model';
5
7
  import { safeToJsDate } from '@dereekb/date';
6
8
  import { Injectable, Inject, Controller } from '@nestjs/common';
7
9
  import { ZohoAccountsOAuthApi } from '@dereekb/zoho/nestjs';
8
10
  import { ConfigModule } from '@nestjs/config';
9
- import { FirebaseServerEnvService } from '@dereekb/firebase-server';
10
11
 
11
12
  /**
12
13
  * {@link SystemState} type identifier for storing Zoho access tokens in Firestore.
13
14
  */ var ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE = 'zoho_access_token';
14
- var zohoAccessTokenSystemStateEmbeddedTokenConverter = firestoreSubObject({
15
- objectField: {
16
- fields: {
17
- key: firestoreString(),
18
- accessToken: firestoreString(),
19
- scope: firestoreString(),
20
- apiDomain: firestoreString(),
21
- expiresIn: firestoreNumber({
22
- default: 3600
23
- }),
24
- expiresAt: firestoreDate()
15
+ /**
16
+ * Creates the embedded-token converter, encrypting the `accessToken` at rest.
17
+ *
18
+ * This is a factory rather than a module-level const because `firestoreEncryptedField` resolves and
19
+ * validates the encryption key eagerly at construction — the secret must be known at runtime.
20
+ *
21
+ * ONLY `accessToken` is encrypted, deliberately. `firestoreEncryptedField` round-trips through
22
+ * `JSON.stringify`/`JSON.parse`, so anything placed inside the encrypted blob loses its type — and
23
+ * `expiresAt` is a `Date`. Encrypting the token string alone keeps every date outside the blob,
24
+ * which is what lets {@link zohoAccessTokenSystemStateDataConverterFactory} keep filtering expired
25
+ * entries without having to decrypt them first. Do not "improve" this by encrypting the whole
26
+ * token object or the `tokens` array.
27
+ *
28
+ * Accepted trade-off: `key`, `scope`, `apiDomain`, `expiresIn` and `expiresAt` remain plaintext at
29
+ * rest. None of them is a credential.
30
+ *
31
+ * @param config - The encryption configuration.
32
+ * @returns The embedded token field converter.
33
+ */ function zohoAccessTokenSystemStateEmbeddedTokenConverterFactory(config) {
34
+ return firestoreSubObject({
35
+ objectField: {
36
+ fields: {
37
+ key: firestoreString(),
38
+ accessToken: firestoreEncryptedField({
39
+ secret: config.encryptionSecret,
40
+ default: '',
41
+ // This is a cache of ~1h tokens, so an undecryptable entry is a cache MISS, not an error.
42
+ // The empty sentinel is dropped by the `tokens` filter, and the next Zoho call re-mints.
43
+ // This is also what makes a rotated secret survivable here (unlike uecp/jwks).
44
+ onDecodeFailure: function onDecodeFailure() {
45
+ return '';
46
+ }
47
+ }),
48
+ scope: firestoreString(),
49
+ apiDomain: firestoreString(),
50
+ expiresIn: firestoreNumber({
51
+ default: 3600
52
+ }),
53
+ expiresAt: firestoreDate()
54
+ }
25
55
  }
26
- }
27
- });
56
+ });
57
+ }
28
58
  /**
29
59
  * Firestore field converter for {@link ZohoAccessTokenSystemStateData}.
30
60
  *
31
61
  * Automatically filters out expired tokens on read and enforces uniqueness by service key.
32
62
  * Must be registered in the app's {@link SystemStateStoredDataConverterMap} under
33
63
  * the {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} key.
34
- */ var zohoAccessTokenSystemStateDataConverter = firestoreSubObject({
35
- objectField: {
36
- fields: {
37
- tokens: firestoreObjectArray({
38
- firestoreField: zohoAccessTokenSystemStateEmbeddedTokenConverter,
39
- filterUnique: filterUniqueFunction(function(x) {
40
- return x.key;
64
+ */ /**
65
+ * Builds the {@link ZohoAccessTokenSystemStateData} converter around a given embedded-token converter.
66
+ *
67
+ * Shared by the encrypted factory and the deprecated plaintext const so the array's expiry filter and
68
+ * per-key dedup behavior can only ever be defined once.
69
+ *
70
+ * @param embeddedTokenConverter - The converter for each entry in the `tokens` array.
71
+ * @returns The stored-data field converter.
72
+ */ function zohoAccessTokenSystemStateDataConverterForEmbeddedTokenConverter(embeddedTokenConverter) {
73
+ return firestoreSubObject({
74
+ objectField: {
75
+ fields: {
76
+ tokens: firestoreObjectArray({
77
+ firestoreField: embeddedTokenConverter,
78
+ filterUnique: filterUniqueFunction(function(x) {
79
+ return x.key;
80
+ }),
81
+ // `firestoreObjectArray` maps BEFORE it filters, so this runs on already-decoded entries.
82
+ // The `accessToken` check is what drops an entry whose decryption failed (onDecodeFailure
83
+ // leaves an empty string behind) — without it such an entry would surface as a token with
84
+ // an empty secret rather than as a cache miss.
85
+ filter: function filter(x) {
86
+ return Boolean(x === null || x === void 0 ? void 0 : x.accessToken) && ((x === null || x === void 0 ? void 0 : x.expiresAt) ? !isPast(x.expiresAt) : true // filter out empty/expired values
87
+ );
88
+ }
41
89
  }),
42
- filter: function filter(x) {
43
- return (x === null || x === void 0 ? void 0 : x.expiresAt) ? !isPast(x.expiresAt) : true // filter out expired values or values that have no expiration
44
- ;
45
- }
46
- }),
47
- lat: firestoreDate({
48
- saveDefaultAsNow: true
49
- })
90
+ lat: firestoreDate({
91
+ saveDefaultAsNow: true
92
+ })
93
+ }
50
94
  }
51
- }
52
- });
95
+ });
96
+ }
97
+ /**
98
+ * Creates the {@link ZohoAccessTokenSystemStateData} converter, encrypting each token's
99
+ * `accessToken` at rest.
100
+ *
101
+ * Register the result in a SERVER-ONLY converter map under {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} —
102
+ * see `systemStatePrivateFirestoreCollection()` in `@dereekb/firebase-server/model`. It must never be
103
+ * registered in an app's client-shared `SystemStateStoredDataConverterMap`.
104
+ *
105
+ * @param config - The encryption configuration.
106
+ * @returns The stored-data field converter.
107
+ */ function zohoAccessTokenSystemStateDataConverterFactory(config) {
108
+ return zohoAccessTokenSystemStateDataConverterForEmbeddedTokenConverter(zohoAccessTokenSystemStateEmbeddedTokenConverterFactory(config));
109
+ }
53
110
  /**
54
111
  * Loads the {@link SystemStateDocument} that stores {@link ZohoAccessTokenSystemStateData},
55
112
  * using {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE} as the document ID.
@@ -65,6 +122,28 @@ var zohoAccessTokenSystemStateEmbeddedTokenConverter = firestoreSubObject({
65
122
  */ function loadZohoAccessTokenSystemState(accessor) {
66
123
  return accessor.loadDocumentForId(ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE);
67
124
  }
125
+ // COMPAT: Deprecated aliases
126
+ /**
127
+ * @deprecated stores the access token in PLAINTEXT. Use
128
+ * {@link zohoAccessTokenSystemStateEmbeddedTokenConverterFactory} instead, which encrypts it at rest.
129
+ */ var zohoAccessTokenSystemStateEmbeddedTokenConverter = firestoreSubObject({
130
+ objectField: {
131
+ fields: {
132
+ key: firestoreString(),
133
+ accessToken: firestoreString(),
134
+ scope: firestoreString(),
135
+ apiDomain: firestoreString(),
136
+ expiresIn: firestoreNumber({
137
+ default: 3600
138
+ }),
139
+ expiresAt: firestoreDate()
140
+ }
141
+ }
142
+ });
143
+ /**
144
+ * @deprecated stores access tokens in PLAINTEXT. Use {@link zohoAccessTokenSystemStateDataConverterFactory}
145
+ * instead, and register it on a server-only SystemStatePrivate collection.
146
+ */ var zohoAccessTokenSystemStateDataConverter = zohoAccessTokenSystemStateDataConverterForEmbeddedTokenConverter(zohoAccessTokenSystemStateEmbeddedTokenConverter);
68
147
 
69
148
  function _array_like_to_array$1(arr, len) {
70
149
  if (len == null || len > arr.length) len = arr.length;
@@ -103,7 +182,7 @@ function _async_to_generator$2(fn) {
103
182
  });
104
183
  };
105
184
  }
106
- function _define_property$4(obj, key, value) {
185
+ function _define_property$5(obj, key, value) {
107
186
  if (key in obj) {
108
187
  Object.defineProperty(obj, key, {
109
188
  value: value,
@@ -132,7 +211,7 @@ function _object_spread$2(target) {
132
211
  }));
133
212
  }
134
213
  ownKeys.forEach(function(key) {
135
- _define_property$4(target, key, source[key]);
214
+ _define_property$5(target, key, source[key]);
136
215
  });
137
216
  }
138
217
  return target;
@@ -273,12 +352,19 @@ function _ts_generator$2(thisArg, body) {
273
352
  * Tokens are stored in a single {@link SystemState} document (type {@link ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE})
274
353
  * and token updates/clears use Firestore transactions for concurrency safety.
275
354
  *
355
+ * The access token is a credential, so pass a SERVER-ONLY collection — a
356
+ * `systemStatePrivateFirestoreCollection()` from `@dereekb/firebase-server/model`, whose converter
357
+ * map registers {@link zohoAccessTokenSystemStateDataConverterFactory} and encrypts the token at
358
+ * rest. Passing an app's client-shared `systemStateCollection` still type-checks (for backwards
359
+ * compatibility) but requires the converter to be registered in the client-shared map, which drags
360
+ * `@dereekb/firebase-server` into browser builds.
361
+ *
276
362
  * @param systemStateCollection - The Firestore collection for system state documents.
277
363
  * @returns A cache service backed by Firestore system state documents.
278
364
  *
279
365
  * @example
280
366
  * ```ts
281
- * const cacheService = firebaseZohoAccountsAccessTokenCacheService(systemStateCollection);
367
+ * const cacheService = firebaseZohoAccountsAccessTokenCacheService(systemStatePrivateCollection);
282
368
  * const cache = cacheService.loadZohoAccessTokenCache('my-zoho-service');
283
369
  * const token = await cache.loadCachedToken();
284
370
  * ```
@@ -454,6 +540,78 @@ function _ts_generator$2(thisArg, body) {
454
540
  return service;
455
541
  }
456
542
 
543
+ function _define_property$4(obj, key, value) {
544
+ if (key in obj) {
545
+ Object.defineProperty(obj, key, {
546
+ value: value,
547
+ enumerable: true,
548
+ configurable: true,
549
+ writable: true
550
+ });
551
+ } else {
552
+ obj[key] = value;
553
+ }
554
+ return obj;
555
+ }
556
+ // MARK: Environment Variable Keys
557
+ /**
558
+ * Environment variable name for the Zoho access token cache encryption secret
559
+ * (hex-encoded AES-256 key).
560
+ *
561
+ * There is NO key rotation — `firestoreEncryptedField` resolves and validates the key once at
562
+ * converter construction and closes over it. Unlike the `uecp` and `oidcJwksKey` secrets, however,
563
+ * rotating this one is SURVIVABLE: the Zoho converter supplies an `onDecodeFailure` handler, so
564
+ * every entry written under the old key simply degrades to a cache miss and the next Zoho call
565
+ * re-mints a token. Rotation costs one extra token request per service key, not an outage.
566
+ */ var ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY = 'ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET';
567
+ /**
568
+ * Deterministic secret used when running in a testing environment and no real secret is configured,
569
+ * so specs never need a live credential.
570
+ *
571
+ * Deliberately distinct from the OIDC JWKS and UserExternalConnection testing secrets so a leaked
572
+ * emulator blob is attributable. ("Zoho Access Token Cache Test Key", hex-encoded.)
573
+ */ var TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET = '5a6f686f2041636365737320546f6b656e2043616368652054657374204b6579';
574
+ // MARK: Config
575
+ /**
576
+ * Reads the Zoho access token encryption secret from the environment.
577
+ *
578
+ * @param configService - The Nest config service used to read the encryption secret.
579
+ * @param envService - Used to detect a testing environment for the secret fallback.
580
+ * @returns The validated encryption secret.
581
+ * @throws {Error} When the configured secret is invalid outside a testing environment.
582
+ */ function zohoAccessTokenEncryptionSecretFactory(configService, envService) {
583
+ var _configService_get;
584
+ var encryptionSecret = (_configService_get = configService.get(ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY)) !== null && _configService_get !== void 0 ? _configService_get : '';
585
+ if (!isValidAES256GCMEncryptionSecret(encryptionSecret)) {
586
+ if (envService.isTestingEnv) {
587
+ encryptionSecret = TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET;
588
+ } else {
589
+ throw new Error("zohoAccessTokenEncryptionSecretFactory: The secret provided by ".concat(ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY, " is not valid. Expected a 64-character hexadecimal string."));
590
+ }
591
+ }
592
+ return encryptionSecret;
593
+ }
594
+ // MARK: Converter Map Entry
595
+ /**
596
+ * Builds the converter map entry for the Zoho access token cache, for use in a SERVER-ONLY
597
+ * SystemState converter map.
598
+ *
599
+ * @param config - The encryption configuration.
600
+ * @returns A partial converter map containing only the Zoho access token entry.
601
+ *
602
+ * @example
603
+ * ```typescript
604
+ * const collections = systemStatePrivateFirestoreCollection({
605
+ * firestoreContext,
606
+ * converters: {
607
+ * ...zohoAccessTokenSystemStatePrivateConverterEntry({ encryptionSecret })
608
+ * }
609
+ * });
610
+ * ```
611
+ */ function zohoAccessTokenSystemStatePrivateConverterEntry(config) {
612
+ return _define_property$4({}, ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE, zohoAccessTokenSystemStateDataConverterFactory(config));
613
+ }
614
+
457
615
  function _type_of$3(obj) {
458
616
  "@swc/helpers - typeof";
459
617
  return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj;
@@ -1623,4 +1781,4 @@ function _unsupported_iterable_to_array(o, minLen) {
1623
1781
  };
1624
1782
  }
1625
1783
 
1626
- export { DEFAULT_ZOHO_OAUTH_SCOPES, ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE, ZOHO_EXTRA_ACCOUNTS_SERVER_KEY, ZOHO_EXTRA_API_DOMAIN_KEY, ZOHO_EXTRA_LOCATION_KEY, ZOHO_OAUTH_CALLBACK_ACCOUNTS_SERVER_PARAM, ZOHO_OAUTH_CALLBACK_LOCATION_PARAM, ZOHO_USER_EXTERNAL_CONNECTION_OAUTH_CONTROLLER_PATH, ZOHO_USER_EXTERNAL_CONNECTION_OAUTH_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, ZohoUserExternalConnectionOAuthController, ZohoUserExternalConnectionOAuthService, ZohoUserExternalConnectionOAuthServiceConfig, appZohoUserExternalConnectionOAuthModuleMetadata, firebaseZohoAccountsAccessTokenCacheService, loadZohoAccessTokenSystemState, userExternalConnectionZohoAccessTokenCache, zohoAccessTokenFromUserExternalConnectionCredentials, zohoAccessTokenSystemStateDataConverter, zohoAccessTokenSystemStateEmbeddedTokenConverter, zohoUserExternalConnectionCredentials, zohoUserExternalConnectionOAuthServiceConfigFactory };
1784
+ export { DEFAULT_ZOHO_OAUTH_SCOPES, TESTING_ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET, ZOHO_ACCESS_TOKEN_ENCRYPTION_SECRET_ENV_KEY, ZOHO_ACCESS_TOKEN_SYSTEM_STATE_TYPE, ZOHO_EXTRA_ACCOUNTS_SERVER_KEY, ZOHO_EXTRA_API_DOMAIN_KEY, ZOHO_EXTRA_LOCATION_KEY, ZOHO_OAUTH_CALLBACK_ACCOUNTS_SERVER_PARAM, ZOHO_OAUTH_CALLBACK_LOCATION_PARAM, ZOHO_USER_EXTERNAL_CONNECTION_OAUTH_CONTROLLER_PATH, ZOHO_USER_EXTERNAL_CONNECTION_OAUTH_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, ZohoUserExternalConnectionOAuthController, ZohoUserExternalConnectionOAuthService, ZohoUserExternalConnectionOAuthServiceConfig, appZohoUserExternalConnectionOAuthModuleMetadata, firebaseZohoAccountsAccessTokenCacheService, loadZohoAccessTokenSystemState, userExternalConnectionZohoAccessTokenCache, zohoAccessTokenEncryptionSecretFactory, zohoAccessTokenFromUserExternalConnectionCredentials, zohoAccessTokenSystemStateDataConverter, zohoAccessTokenSystemStateDataConverterFactory, zohoAccessTokenSystemStateEmbeddedTokenConverter, zohoAccessTokenSystemStateEmbeddedTokenConverterFactory, zohoAccessTokenSystemStatePrivateConverterEntry, zohoUserExternalConnectionCredentials, zohoUserExternalConnectionOAuthServiceConfigFactory };
package/zoho/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/zoho",
3
- "version": "13.33.0",
3
+ "version": "13.34.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.33.0",
6
- "@dereekb/date": "13.33.0",
7
- "@dereekb/model": "13.33.0",
8
- "@dereekb/nestjs": "13.33.0",
9
- "@dereekb/rxjs": "13.33.0",
10
- "@dereekb/firebase": "13.33.0",
11
- "@dereekb/firebase-server": "13.33.0",
12
- "@dereekb/util": "13.33.0",
13
- "@dereekb/zoho": "13.33.0",
5
+ "@dereekb/analytics": "13.34.0",
6
+ "@dereekb/date": "13.34.0",
7
+ "@dereekb/model": "13.34.0",
8
+ "@dereekb/nestjs": "13.34.0",
9
+ "@dereekb/rxjs": "13.34.0",
10
+ "@dereekb/firebase": "13.34.0",
11
+ "@dereekb/firebase-server": "13.34.0",
12
+ "@dereekb/util": "13.34.0",
13
+ "@dereekb/zoho": "13.34.0",
14
14
  "@nestjs/common": "^11.1.19",
15
+ "@nestjs/config": "^4.0.4",
15
16
  "express": "^5.2.1"
16
17
  },
17
18
  "exports": {
@@ -1,4 +1,5 @@
1
1
  export * from './zoho.accounts.firebase';
2
+ export * from './zoho.accounts.firebase.module';
2
3
  export * from './zoho.accounts.firebase.system';
3
4
  export * from './zoho.oauth.connection.cache';
4
5
  export * from './zoho.oauth.connection.config';