oracledb 7.0.1 → 26.0.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.
Files changed (74) hide show
  1. package/README.md +16 -6
  2. package/build/Release/oracledb-26.0.0-darwin-arm64.node +0 -0
  3. package/build/Release/oracledb-26.0.0-darwin-arm64.node-buildinfo.txt +1 -0
  4. package/build/Release/oracledb-26.0.0-js-buildinfo.txt +1 -0
  5. package/build/Release/oracledb-26.0.0-linux-arm64.node +0 -0
  6. package/build/Release/oracledb-26.0.0-linux-arm64.node-buildinfo.txt +1 -0
  7. package/build/Release/oracledb-26.0.0-linux-x64.node +0 -0
  8. package/build/Release/oracledb-26.0.0-linux-x64.node-buildinfo.txt +1 -0
  9. package/build/Release/oracledb-26.0.0-win32-x64.node +0 -0
  10. package/build/Release/oracledb-26.0.0-win32-x64.node-buildinfo.txt +1 -0
  11. package/lib/connection.js +71 -8
  12. package/lib/constants.js +4 -0
  13. package/lib/endUserSecurityContext.js +20 -8
  14. package/lib/errors.js +70 -5
  15. package/lib/impl/connection.js +36 -0
  16. package/lib/impl/datahandlers/buffer.js +91 -18
  17. package/lib/impl/datahandlers/oson.js +3 -2
  18. package/lib/impl/resultset.js +32 -11
  19. package/lib/oracledb.js +34 -0
  20. package/lib/pool.js +6 -2
  21. package/lib/queryStream.js +13 -5
  22. package/lib/resultset.js +10 -3
  23. package/lib/securityContextProvider.js +206 -0
  24. package/lib/thin/aq.js +2 -3
  25. package/lib/thin/connection.js +381 -19
  26. package/lib/thin/dbObject.js +17 -8
  27. package/lib/thin/pool.js +3 -2
  28. package/lib/thin/protocol/capabilities.js +37 -4
  29. package/lib/thin/protocol/constants.js +49 -1
  30. package/lib/thin/protocol/messages/auth.js +17 -6
  31. package/lib/thin/protocol/messages/base.js +74 -16
  32. package/lib/thin/protocol/messages/dataType.js +46 -0
  33. package/lib/thin/protocol/messages/directPathLoadStream.js +3 -2
  34. package/lib/thin/protocol/messages/execute.js +10 -4
  35. package/lib/thin/protocol/messages/index.js +4 -0
  36. package/lib/thin/protocol/messages/notification.js +281 -0
  37. package/lib/thin/protocol/messages/subscribe.js +181 -0
  38. package/lib/thin/protocol/messages/withData.js +21 -7
  39. package/lib/thin/protocol/packet.js +2 -4
  40. package/lib/thin/protocol/protocol.js +7 -4
  41. package/lib/thin/resultSet.js +40 -20
  42. package/lib/thin/sqlnet/ezConnectResolver.js +7 -0
  43. package/lib/thin/sqlnet/navNodes.js +3 -3
  44. package/lib/thin/sqlnet/ntTcp.js +24 -8
  45. package/lib/thin/statement.js +6 -1
  46. package/lib/thin/statementCache.js +34 -6
  47. package/lib/thin/subscr.js +363 -0
  48. package/lib/transformer.js +60 -17
  49. package/lib/util.js +46 -8
  50. package/lib/version.js +2 -2
  51. package/package.json +1 -1
  52. package/plugins/configProviders/gcpstorage/index.js +121 -0
  53. package/plugins/configProviders/ociobject/index.js +1 -1
  54. package/plugins/token/cache.js +154 -0
  55. package/plugins/token/endUserSecurityProvider/index.js +377 -0
  56. package/plugins/token/extensionAzure/index.js +160 -28
  57. package/plugins/token/extensionOci/index.js +221 -16
  58. package/plugins/vectorsdk/OracleVecDB.js +346 -0
  59. package/plugins/vectorsdk/index.js +49 -0
  60. package/plugins/vectorsdk/utils.js +370 -0
  61. package/plugins/vectorsdk/vectorEmbedding.js +223 -0
  62. package/plugins/vectorsdk/vectorIndex.js +177 -0
  63. package/plugins/vectorsdk/vectorRecords.js +406 -0
  64. package/plugins/vectorsdk/vectorSchema.js +408 -0
  65. package/plugins/vectorsdk/vectorSearch.js +538 -0
  66. package/build/Release/oracledb-7.0.1-darwin-arm64.node +0 -0
  67. package/build/Release/oracledb-7.0.1-darwin-arm64.node-buildinfo.txt +0 -1
  68. package/build/Release/oracledb-7.0.1-js-buildinfo.txt +0 -1
  69. package/build/Release/oracledb-7.0.1-linux-arm64.node +0 -0
  70. package/build/Release/oracledb-7.0.1-linux-arm64.node-buildinfo.txt +0 -1
  71. package/build/Release/oracledb-7.0.1-linux-x64.node +0 -0
  72. package/build/Release/oracledb-7.0.1-linux-x64.node-buildinfo.txt +0 -1
  73. package/build/Release/oracledb-7.0.1-win32-x64.node +0 -0
  74. package/build/Release/oracledb-7.0.1-win32-x64.node-buildinfo.txt +0 -1
@@ -0,0 +1,377 @@
1
+ // Copyright (c) 2026, Oracle and/or its affiliates.
2
+
3
+ //-----------------------------------------------------------------------------
4
+ //
5
+ // This software is dual-licensed to you under the Universal Permissive License
6
+ // (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
7
+ // 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
8
+ // either license.
9
+ //
10
+ // If you elect to accept the software under the Apache License, Version 2.0,
11
+ // the following applies:
12
+ //
13
+ // Licensed under the Apache License, Version 2.0 (the "License");
14
+ // you may not use this file except in compliance with the License.
15
+ // You may obtain a copy of the License at
16
+ //
17
+ // https://www.apache.org/licenses/LICENSE-2.0
18
+ //
19
+ // Unless required by applicable law or agreed to in writing, software
20
+ // distributed under the License is distributed on an "AS IS" BASIS,
21
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
+ // See the License for the specific language governing permissions and
23
+ // limitations under the License.
24
+ //
25
+ //-----------------------------------------------------------------------------
26
+ 'use strict';
27
+
28
+ /**
29
+ * End-user security context token provider. Each configured pool or standalone
30
+ * connection owns its provider; there is no process-global provider setting.
31
+ */
32
+ const oracledb = require('oracledb');
33
+ const securityContextProvider = oracledb.getSecurityContextProvider();
34
+
35
+ const hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
36
+ // These endUserSecParams fields are consumed by this provider and must not be
37
+ // forwarded to an Azure or OCI token extension as token configuration.
38
+ const PROVIDER_CONTROL_FIELDS = new Set([
39
+ 'spiType', 'authFlow', 'endUserToken', 'endUserName',
40
+ 'dataRoles', 'attributes', 'key',
41
+ 'cacheOptions', 'contextResolution'
42
+ ]);
43
+ const DEFAULT_METADATA_FIELDS = [
44
+ 'endUserToken', 'endUserName', 'dataRoles', 'attributes', 'key',
45
+ ];
46
+ const PROVIDER_MODULE_PATHS = {
47
+ azure: '../extensionAzure/index.js',
48
+ oci: '../extensionOci/index.js',
49
+ };
50
+
51
+ function throwErr(message, code) {
52
+ const err = new Error(message);
53
+ if (code) {
54
+ err.code = code;
55
+ }
56
+ throw err;
57
+ }
58
+
59
+ function ensureInvocationHook() {
60
+ if (!securityContextProvider.getInvocationHook?.()) {
61
+ securityContextProvider.setInvocationHook(
62
+ securityContextProvider.createInvocationHook()
63
+ );
64
+ }
65
+ }
66
+
67
+ function normalizeAuthFlow(value) {
68
+ const flow = String(value ?? 'onBehalfOf').trim().toLowerCase();
69
+ if (flow === 'app' || flow === 'clientcredentials' ||
70
+ flow === 'client_credentials' || flow === 'client-credentials') {
71
+ return 'app';
72
+ }
73
+ if (flow === 'obo' || flow === 'onbehalfof' ||
74
+ flow === 'on_behalf_of' || flow === 'on-behalf-of') {
75
+ return 'obo';
76
+ }
77
+ throwErr("endUserSecParams.authFlow must be 'app' or 'onBehalfOf'.");
78
+ }
79
+
80
+ function normalizeContextResolution(value) {
81
+ const resolution = String(value ?? 'connection').trim().toLowerCase();
82
+ if (resolution === 'connection' || resolution === 'operation') {
83
+ return resolution;
84
+ }
85
+ throwErr("endUserSecParams.contextResolution must be 'connection' or 'operation'.");
86
+ }
87
+
88
+ function normalizeProvider(params) {
89
+ const spiType = String(params.spiType ?? 'oci').trim().toLowerCase();
90
+ const modulePath = PROVIDER_MODULE_PATHS[spiType];
91
+ if (!modulePath) {
92
+ throwErr("endUserSecParams.spiType must be 'azure' or 'oci'.");
93
+ }
94
+ // Start with the supplied parameters, then remove fields interpreted by this
95
+ // end-user-security provider. The remaining tokenConfig is passed only to
96
+ // the selected Azure or OCI extension, so request metadata and provider
97
+ // controls cannot accidentally be treated as token-client options.
98
+ const tokenConfig = { ...params };
99
+ for (const key of PROVIDER_CONTROL_FIELDS) {
100
+ delete tokenConfig[key];
101
+ }
102
+ // Azure's application-token helper expects this value. It is harmless for
103
+ // OBO-capable configurations because both flows use the same credentials.
104
+ if (spiType === 'azure' && !tokenConfig.authType) {
105
+ tokenConfig.authType = 'azureserviceprincipal';
106
+ }
107
+ return {
108
+ vendor: spiType,
109
+ modulePath,
110
+ tokenConfig,
111
+ cacheOptions: params.cacheOptions ? { ...params.cacheOptions } : undefined
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Creates a provider from the `endUserSecParams` shape. Configuration fields
117
+ * that describe the identity provider are static. `endUserToken`,
118
+ * `endUserName`, `key`, `dataRoles`, and `attributes` are defaults only: request
119
+ * metadata supplied by runWithContext() wins.
120
+ */
121
+ function configureUnifiedContextProvider(params) {
122
+ ensureInvocationHook();
123
+ const config = normalizeConfig(params);
124
+ const provider = loadProviderModule(config.provider);
125
+ return createProviderFn(config, provider);
126
+ }
127
+
128
+ function mergeMetadata(defaults, current) {
129
+ if (!current || typeof current !== 'object') {
130
+ return defaults;
131
+ }
132
+ return { ...defaults, ...current };
133
+ }
134
+
135
+ function getEndUserToken(metadata) {
136
+ return metadata.endUserToken;
137
+ }
138
+
139
+ function createProviderFn(config, provider) {
140
+ const unifiedProvider = async function unifiedProvider() {
141
+ const current = securityContextProvider.getCurrentContext();
142
+ if (!current || typeof current !== "object") {
143
+ // Do not acquire an application token for normal work outside a scope.
144
+ return undefined;
145
+ }
146
+ if (hasOwn(current, 'authorization')) {
147
+ throwErr('Security context metadata.authorization is not supported; use endUserToken.');
148
+ }
149
+ const metadata = mergeMetadata(config.defaultMetadata, current);
150
+ const endUserToken = getEndUserToken(metadata);
151
+ // authFlow is fixed when the pool or standalone connection is created.
152
+ // Per-request metadata supplies identity only; it cannot select a token
153
+ // exchange for which the configured provider may lack credentials.
154
+ if (config.authFlow === "obo") {
155
+ if (config.provider.vendor !== 'azure') {
156
+ throwErr('On-behalf-of security contexts are supported only with spiType "azure".');
157
+ }
158
+ if (!endUserToken) {
159
+ throwErr("authFlow=obo requires an end-user token.");
160
+ }
161
+
162
+ const tokenResult = await acquireOboToken(config, provider, endUserToken);
163
+ return getOrCreateScopedSecurityContext(unifiedProvider, config,
164
+ tokenResult, () => createSecurityContext(finalizeOboMode(metadata,
165
+ tokenResult.accessToken)));
166
+ }
167
+
168
+ const tokenResult = await acquireAppToken(config, provider);
169
+ const databaseAccessToken = tokenResult.accessToken;
170
+ // No end-user identity means the database resolves the application
171
+ // identity directly from the client-credential database access token.
172
+ if (!endUserToken && !metadata.endUserName) {
173
+ // A key identifies a named database-managed end user. It is not valid
174
+ // for direct application logon, but roles and attributes are valid
175
+ // authorization metadata for either identity form.
176
+ if (metadata.key) {
177
+ throwErr("authFlow 'app' requires endUserName when key is supplied.");
178
+ }
179
+ return getOrCreateScopedSecurityContext(unifiedProvider, config,
180
+ tokenResult, () => createSecurityContext(
181
+ buildContext({
182
+ databaseAccessToken,
183
+ dataRoles: metadata.dataRoles,
184
+ attributes: metadata.attributes
185
+ })));
186
+ }
187
+ // App mode uses the application token to authorize database access and
188
+ // carries a supplied end-user token directly in the EUSC. This is not an
189
+ // OBO exchange; Azure and OCI both support this explicit identity form.
190
+ if (endUserToken) {
191
+ if (metadata.endUserName || metadata.key) {
192
+ throwErr('App mode cannot combine endUserToken with endUserName or key.');
193
+ }
194
+ return getOrCreateScopedSecurityContext(unifiedProvider, config,
195
+ tokenResult, () => createSecurityContext(finalizeAppTokenMode(metadata,
196
+ databaseAccessToken)));
197
+ }
198
+ if (!metadata.endUserName) {
199
+ throwErr("authFlow 'app' requires endUserName when endUserToken is supplied.");
200
+ }
201
+ return getOrCreateScopedSecurityContext(unifiedProvider, config,
202
+ tokenResult, () => createSecurityContext(
203
+ finalizeAppMode(metadata, databaseAccessToken)));
204
+ };
205
+ // The driver reads this private property to choose the provider lifecycle.
206
+ unifiedProvider._contextResolution = config.contextResolution;
207
+ return unifiedProvider;
208
+ }
209
+
210
+ function createSecurityContext(context) {
211
+ return new oracledb.EndUserSecurityContext(context);
212
+ }
213
+
214
+ // Operation resolution calls the provider for every database operation so the
215
+ // token extension remains authoritative for expiry and refresh. When it
216
+ // confirms the same database token came from its cache, reuse the already
217
+ // encoded EUSC for this runWithContext() scope. The scope cache holds no
218
+ // plaintext token and is released when the async scope completes.
219
+ function getOrCreateScopedSecurityContext(provider, config, tokenResult,
220
+ createContext) {
221
+ if (config.contextResolution !== 'operation') {
222
+ return createContext();
223
+ }
224
+ const cached = securityContextProvider._getScopedValue(provider);
225
+ if (cached && tokenResult.isNewToken === false) {
226
+ return cached;
227
+ }
228
+ const securityContext = createContext();
229
+ securityContextProvider._setScopedValue(provider, securityContext);
230
+ return securityContext;
231
+ }
232
+
233
+ function finalizeOboMode(metadata, databaseAccessToken) {
234
+ const attributeResolution = resolveAttributesForRequest(metadata);
235
+ const context = {
236
+ databaseAccessToken,
237
+ dataRoles: metadata.dataRoles,
238
+ attributes: attributeResolution.payload
239
+ };
240
+ context.endUserToken = getEndUserToken(metadata);
241
+ return buildContext(context);
242
+ }
243
+
244
+ function finalizeAppMode(metadata, databaseAccessToken) {
245
+ const context = buildContext({
246
+ databaseAccessToken,
247
+ endUserName: metadata.endUserName,
248
+ dataRoles: metadata.dataRoles,
249
+ key: metadata.key,
250
+ attributes: metadata.attributes
251
+ });
252
+ return context;
253
+ }
254
+
255
+ function finalizeAppTokenMode(metadata, databaseAccessToken) {
256
+ return buildContext({
257
+ databaseAccessToken,
258
+ endUserToken: metadata.endUserToken,
259
+ dataRoles: metadata.dataRoles,
260
+ attributes: metadata.attributes
261
+ });
262
+ }
263
+
264
+ async function acquireAppToken(config, provider) {
265
+ const result = await provider.getTokenResult(config.provider.tokenConfig);
266
+ return {
267
+ accessToken: getAccessToken(result,
268
+ 'Token provider failed to return an application access token.'),
269
+ isNewToken: result?.isNewToken !== false,
270
+ };
271
+ }
272
+
273
+ async function acquireOboToken(config, provider, endUserToken) {
274
+ const result = await provider.getOnBehalfOfToken({
275
+ endUserToken,
276
+ oboConfig: config.provider.tokenConfig,
277
+ cacheOptions: config.provider.cacheOptions
278
+ });
279
+ const accessToken = getAccessToken(result,
280
+ 'Token provider failed to return an access token for on-behalf-of exchange.');
281
+ return {
282
+ accessToken,
283
+ // Extensions set false only when returning a valid cached database token.
284
+ // Unknown/custom provider results are conservative and force a new EUSC.
285
+ isNewToken: result?.isNewToken !== false,
286
+ };
287
+ }
288
+
289
+ function getAccessToken(result, errorMessage) {
290
+ const accessToken = typeof result === 'string'
291
+ ? result
292
+ : (result?.accessToken ?? result?.token);
293
+ if (typeof accessToken !== 'string' || !accessToken.trim()) {
294
+ throwErr(errorMessage);
295
+ }
296
+ return accessToken;
297
+ }
298
+
299
+ function loadProviderModule(descriptor) {
300
+ return require(descriptor.modulePath);
301
+ }
302
+
303
+ function buildContext(base) {
304
+ const context = { databaseAccessToken: base.databaseAccessToken };
305
+ for (const key of ['endUserToken', 'endUserName', 'dataRoles', 'key', 'attributes']) {
306
+ if (base[key] !== undefined) {
307
+ context[key] = base[key];
308
+ }
309
+ }
310
+ return context;
311
+ }
312
+
313
+ function normalizeConfig(params) {
314
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
315
+ throw new TypeError('endUserSecParams must be an object.');
316
+ }
317
+ const defaultMetadata = {};
318
+ for (const key of DEFAULT_METADATA_FIELDS) {
319
+ if (hasOwn(params, key)) {
320
+ defaultMetadata[key] = params[key];
321
+ }
322
+ }
323
+ const authFlow = normalizeAuthFlow(params.authFlow);
324
+ const provider = normalizeProvider(params);
325
+ if (authFlow === 'obo' && provider.vendor !== 'azure') {
326
+ throwErr('On-behalf-of security contexts are supported only with spiType "azure".');
327
+ }
328
+ return {
329
+ authFlow,
330
+ contextResolution: normalizeContextResolution(params.contextResolution),
331
+ defaultMetadata: snapshotDefaultMetadata(defaultMetadata),
332
+ provider
333
+ };
334
+ }
335
+
336
+ // Provider defaults are static after pool/connection creation. Snapshot the
337
+ // mutable fields so changes to the caller's configuration object cannot alter
338
+ // a scope after the provider has been configured.
339
+ function snapshotDefaultMetadata(metadata) {
340
+ const snapshot = { ...metadata };
341
+ if (Array.isArray(metadata.dataRoles)) {
342
+ snapshot.dataRoles = [...metadata.dataRoles];
343
+ }
344
+ if (metadata.attributes && typeof metadata.attributes === 'object') {
345
+ snapshot.attributes = JSON.parse(JSON.stringify(metadata.attributes));
346
+ }
347
+ return snapshot;
348
+ }
349
+
350
+ function resolveAttributesForRequest(metadata) {
351
+ if (!hasOwn(metadata, 'attributes')) {
352
+ return { payload: undefined };
353
+ }
354
+ try {
355
+ JSON.stringify(metadata.attributes);
356
+ } catch (err) {
357
+ const error = new Error('security context attributes must be JSON-serializable.');
358
+ error.cause = err;
359
+ throw error;
360
+ }
361
+ return { payload: metadata.attributes };
362
+ }
363
+
364
+ /**
365
+ * Process configuration hook. `endUserSecParams` is copied to the pool or
366
+ * standalone connection being created. Dynamic request metadata is supplied
367
+ * later with runWithContext().
368
+ */
369
+ function hookFn(options) {
370
+ if (!options.endUserSecParams) {
371
+ return;
372
+ }
373
+ options._securityContextProvider =
374
+ configureUnifiedContextProvider(options.endUserSecParams);
375
+ }
376
+
377
+ oracledb.registerProcessConfigurationHook(hookFn);
@@ -26,9 +26,27 @@
26
26
  'use strict';
27
27
  const oracledb = require('oracledb');
28
28
  const msal = require('@azure/msal-node');
29
+ const { TokenCache, hashValue } = require('../cache.js');
29
30
 
31
+ const OBO_DEFAULT_CLOCK_SKEW_MS = 60 * 1000;
32
+ const TOKEN_CACHE_MAX_ENTRIES = 100;
33
+ // Process-wide, bounded LRU cache of completed OBO exchanges. Connections
34
+ // safely share entries only when their cache key has the same caller-token
35
+ // hash and every Azure credential/request input that can change the result.
36
+ // Values are obfuscated at rest.
37
+ const oboTokenCache = new TokenCache(TOKEN_CACHE_MAX_ENTRIES);
38
+ // Process-wide, bounded cache of service-principal tokens. Its key includes
39
+ // every credential and request input that can change the resulting token, so
40
+ // pools can share only compatible entries. Values are obfuscated at rest.
41
+ const appTokenCache = new TokenCache(TOKEN_CACHE_MAX_ENTRIES);
30
42
 
31
- async function getToken(params) {
43
+
44
+ // Internal Deep Security API. The driver's ordinary token-authentication hook
45
+ // below returns this result's access-token string.
46
+ async function getTokenResult(params) {
47
+ if (typeof params?.authType !== 'string') {
48
+ throwErr('Azure authentication type is required.');
49
+ }
32
50
  switch (params.authType.toLowerCase()) {
33
51
  case 'azureserviceprincipal':
34
52
  return await servicePrincipalCredentials(params);
@@ -46,9 +64,7 @@ function throwErr(message) {
46
64
 
47
65
  //---------------------------------------------------------------------------
48
66
  // Returns the access token for authentication as a service principal. This
49
- // authentication method requires a client ID along with either a secret or
50
- // certificate. These values may be provided as config parameters read by
51
- // the Azure SDK.
67
+ // authentication method requires a client ID and client secret.
52
68
  // ---------------------------------------------------------------------------
53
69
  async function servicePrincipalCredentials(params) {
54
70
  const clientId = params.clientId ??
@@ -57,31 +73,24 @@ async function servicePrincipalCredentials(params) {
57
73
  throwErr("Token based authentication config parameter authority is missing for azureServicePrincipal in extensionAzure plugins.");
58
74
  const clientSecret = params.clientSecret ??
59
75
  throwErr("Token based authentication config parameter clientSecret is missing for azureServicePrincipal in extensionAzure plugins.");
60
- const msalConfig = {
61
- auth: {
62
- clientId: clientId,
63
- authority: authority,
64
- clientSecret: clientSecret,
65
- },
66
- system: {
67
- // Use it while generating azure tokens behind proxy
68
- // Otherwise can remove this option
69
- networkClient: params.proxy,
70
- },
71
- };
72
-
73
- if (!msalConfig.system.networkClient) {
74
- delete msalConfig.system.networkClient;
75
- }
76
-
77
76
  const scopes = params.scopes ??
78
77
  throwErr("Token based authentication config parameter scopes is missing for azureServicePrincipal in extensionAzure plugins.");
79
- const tokenRequest = {
80
- scopes: [scopes],
81
- };
82
- const cca = new msal.ConfidentialClientApplication(msalConfig);
83
- const authResponse = await cca.acquireTokenByClientCredential(tokenRequest);
84
- return authResponse.accessToken;
78
+ const normalizedScopes = normalizeScopes(scopes);
79
+ const config = { ...params, clientId, authority, clientSecret };
80
+ const cacheKey = computeAppCacheKey(config, normalizedScopes);
81
+ const result = await appTokenCache.get(cacheKey, async () => {
82
+ const authResponse = await createClient(config).acquireTokenByClientCredential({
83
+ scopes: normalizedScopes,
84
+ });
85
+ if (!authResponse?.accessToken) {
86
+ throwErr('Failed to acquire application access token from Azure.');
87
+ }
88
+ return {
89
+ value: authResponse.accessToken,
90
+ validUntil: calculateExpiry(authResponse, OBO_DEFAULT_CLOCK_SKEW_MS),
91
+ };
92
+ });
93
+ return { accessToken: result.value, isNewToken: !result.isCacheHit };
85
94
  }
86
95
 
87
96
  // ---------------------------------------------------------------------------
@@ -92,9 +101,132 @@ async function servicePrincipalCredentials(params) {
92
101
  function hookFn(options) {
93
102
  if (options.tokenAuthConfigAzure) {
94
103
  options.accessToken = async function callbackFn(refresh, config) {
95
- return await getToken(config);
104
+ return (await getTokenResult(config)).accessToken;
96
105
  };
97
106
  options.accessTokenConfig = options.tokenAuthConfigAzure;
98
107
  }
99
108
  }
100
109
  oracledb.registerProcessConfigurationHook(hookFn);
110
+
111
+ //---------------------------------------------------------------------------
112
+ // normalizeScopes()
113
+ //---------------------------------------------------------------------------
114
+ function normalizeScopes(scopes) {
115
+ const normalized = Array.isArray(scopes)
116
+ ? scopes
117
+ : (typeof scopes === 'string' ? [scopes] : null);
118
+ if (!normalized || normalized.some((scope) =>
119
+ typeof scope !== 'string' || !scope.trim())) {
120
+ throwErr('Azure scopes must be a string or an array of non-empty strings.');
121
+ }
122
+ return normalized.map((scope) => scope.trim()).sort();
123
+ }
124
+
125
+ //---------------------------------------------------------------------------
126
+ // buildNetworkClient()
127
+ //---------------------------------------------------------------------------
128
+ function buildNetworkClient(proxyConfig) {
129
+ if (!proxyConfig) {
130
+ return null;
131
+ }
132
+ if (typeof proxyConfig.sendGetRequestAsync === 'function' &&
133
+ typeof proxyConfig.sendPostRequestAsync === 'function') {
134
+ return proxyConfig;
135
+ }
136
+ throwErr('Azure proxy configuration must implement sendGetRequestAsync() and sendPostRequestAsync().');
137
+ }
138
+
139
+ //---------------------------------------------------------------------------
140
+ // computeOboCacheKey()
141
+ //---------------------------------------------------------------------------
142
+ function computeOboCacheKey(config, scopes, endUserToken) {
143
+ // Do not put the assertion or client secret itself in a process-wide Map.
144
+ // The secret hash separates entries if a client secret is rotated while the
145
+ // process is running.
146
+ return JSON.stringify([
147
+ config.authType?.toLowerCase(), config.clientId,
148
+ hashValue(config.clientSecret), config.authority, scopes,
149
+ hashValue(endUserToken),
150
+ ]);
151
+ }
152
+
153
+ function computeAppCacheKey(config, scopes) {
154
+ // Hash the secret rather than retaining it in the process-wide key. A
155
+ // changed secret creates a new entry, leaving the old one bounded and
156
+ // eligible for LRU eviction.
157
+ return JSON.stringify([
158
+ config.authType?.toLowerCase(), config.clientId,
159
+ hashValue(config.clientSecret), config.authority, scopes,
160
+ ]);
161
+ }
162
+
163
+ function createClient(config) {
164
+ const networkClient = buildNetworkClient(config.proxy);
165
+ const msalConfig = {
166
+ auth: {
167
+ clientId: config.clientId ?? throwErr("OBO configuration requires clientId."),
168
+ authority: config.authority ?? throwErr("OBO configuration requires authority."),
169
+ clientSecret: config.clientSecret ?? throwErr("OBO configuration requires clientSecret."),
170
+ }
171
+ };
172
+ if (networkClient) {
173
+ msalConfig.system = { networkClient };
174
+ }
175
+
176
+ return new msal.ConfidentialClientApplication(msalConfig);
177
+ }
178
+
179
+ //---------------------------------------------------------------------------
180
+ // calculateExpiry()
181
+ //---------------------------------------------------------------------------
182
+ function calculateExpiry(result, clockSkewMs) {
183
+ if (result?.expiresOn instanceof Date) {
184
+ return result.expiresOn.getTime() - clockSkewMs;
185
+ }
186
+ throwErr('Azure token response did not include an expiration time.');
187
+ }
188
+
189
+ //---------------------------------------------------------------------------
190
+ // getOnBehalfOfToken()
191
+ //---------------------------------------------------------------------------
192
+ async function getOnBehalfOfToken({ endUserToken, oboConfig, cacheOptions }) {
193
+ if (!endUserToken) {
194
+ throwErr('An end user access token must be supplied for OBO exchange.');
195
+ }
196
+ if (!oboConfig) {
197
+ throwErr('OBO configuration is required.');
198
+ }
199
+
200
+ const scopes = normalizeScopes(oboConfig.scopes);
201
+ const cacheEnabled = cacheOptions?.enabled !== false;
202
+ const clockSkewMs = cacheOptions?.clockSkewMs ?? OBO_DEFAULT_CLOCK_SKEW_MS;
203
+ // This process-wide bounded cache is authoritative. Do not use MSAL's
204
+ // per-client cache because clients are intentionally short-lived so they do
205
+ // not retain credentials in a long-lived object graph.
206
+
207
+ const cacheKey = computeOboCacheKey(oboConfig, scopes, endUserToken);
208
+ const result = await oboTokenCache.get(cacheKey, async () => {
209
+ const authResponse = await createClient(oboConfig).acquireTokenOnBehalfOf({
210
+ oboAssertion: endUserToken,
211
+ scopes,
212
+ skipCache: true
213
+ });
214
+ if (!authResponse?.accessToken) {
215
+ throwErr('Failed to acquire on-behalf-of access token from Azure.');
216
+ }
217
+ return {
218
+ value: authResponse.accessToken,
219
+ validUntil: calculateExpiry(authResponse, clockSkewMs),
220
+ };
221
+ }, { enabled: cacheEnabled });
222
+ return {
223
+ accessToken: result.value,
224
+ expiresOn: new Date(result.validUntil + clockSkewMs),
225
+ isNewToken: !result.isCacheHit,
226
+ };
227
+ }
228
+
229
+ module.exports = {
230
+ getTokenResult,
231
+ getOnBehalfOfToken
232
+ };