@mcp-abap-adt/auth-broker 2.2.0 → 3.0.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.
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ /**
3
+ * SAML metadata, read for what a SAML run would otherwise be told by hand.
4
+ *
5
+ * Two documents, two sides:
6
+ * - the identity provider's (`--idp-metadata`): its entityID, its signing
7
+ * certificates and its SSO URL — what the assertion is validated against;
8
+ * - the service provider's, which for XSUAA is `<uaa.url>/saml/metadata`: its
9
+ * entityID (the assertion's Audience) and the bearer endpoint
10
+ * `/oauth/token/alias/<alias>`, which is both the assertion's Recipient and
11
+ * the token endpoint of the saml2-bearer grant.
12
+ *
13
+ * What is read here is trust material. A certificate taken from metadata is
14
+ * only as trustworthy as the channel that delivered it, so a URL must be
15
+ * https; plain http is accepted for loopback hosts only, which is a local test
16
+ * identity provider and nothing else.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.loadMetadata = loadMetadata;
20
+ exports.readIdpMetadata = readIdpMetadata;
21
+ exports.readSpMetadata = readSpMetadata;
22
+ exports.applySamlMetadata = applySamlMetadata;
23
+ const node_fs_1 = require("node:fs");
24
+ const node_path_1 = require("node:path");
25
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
26
+ /** A metadata document from an https URL, a loopback http URL, or a file. */
27
+ async function loadMetadata(source) {
28
+ if (/^https?:\/\//i.test(source)) {
29
+ const url = new URL(source);
30
+ if (url.protocol !== 'https:' && !LOOPBACK.has(url.hostname)) {
31
+ throw new Error(`refusing SAML metadata over ${url.protocol} from ${url.hostname}: it carries the certificates assertions are verified against, so it must come over https`);
32
+ }
33
+ const response = await fetch(url);
34
+ if (!response.ok) {
35
+ throw new Error(`SAML metadata ${source} answered ${response.status}`);
36
+ }
37
+ return response.text();
38
+ }
39
+ return (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(source), 'utf8');
40
+ }
41
+ function attribute(tag, name) {
42
+ return tag.match(new RegExp(`\\s${name}="([^"]*)"`))?.[1];
43
+ }
44
+ /**
45
+ * Every `EntityDescriptor` in the document, whether it is the root or one of
46
+ * many inside an `EntitiesDescriptor` (federation metadata). An entity's
47
+ * entityID, keys and endpoints are read from the same element — taking the
48
+ * first entityID in the document paired one entity's ID with another's
49
+ * certificate.
50
+ */
51
+ function entitiesOf(xml) {
52
+ return [
53
+ ...xml.matchAll(/<(?:[\w-]+:)?EntityDescriptor\b([^>]*)>([\s\S]*?)<\/(?:[\w-]+:)?EntityDescriptor>/g),
54
+ ].map(([, attributes, body]) => ({
55
+ entityId: attribute(attributes, 'entityID'),
56
+ body,
57
+ }));
58
+ }
59
+ /** The inner XML of the first `<prefix:name>` element, whatever the prefix. */
60
+ function section(xml, name) {
61
+ return xml.match(new RegExp(`<(?:[\\w-]+:)?${name}\\b[^>]*>([\\s\\S]*?)</(?:[\\w-]+:)?${name}>`))?.[1];
62
+ }
63
+ /**
64
+ * The one entity the caller means. With a wanted entityID, the entity with
65
+ * it; without one, the only candidate — several are a question only the
66
+ * caller can answer, and picking the first would trust an identity provider
67
+ * nobody named.
68
+ */
69
+ function chooseEntity(candidates, wanted, role, flag) {
70
+ if (wanted !== undefined) {
71
+ const match = candidates.find((entity) => entity.entityId === wanted);
72
+ if (!match) {
73
+ throw new Error(`the metadata has no ${role} with entityID ${JSON.stringify(wanted)}; it has: ${candidates
74
+ .map((entity) => JSON.stringify(entity.entityId))
75
+ .join(', ') || 'none'}`);
76
+ }
77
+ return match;
78
+ }
79
+ if (candidates.length === 0) {
80
+ throw new Error(`the metadata describes no ${role}`);
81
+ }
82
+ if (candidates.length > 1) {
83
+ throw new Error(`the metadata describes ${candidates.length} ${role}s; name the one to use with ${flag}: ${candidates
84
+ .map((entity) => JSON.stringify(entity.entityId))
85
+ .join(', ')}`);
86
+ }
87
+ return candidates[0];
88
+ }
89
+ /**
90
+ * The identity provider in `xml`: the one whose entityID is `entityId`, or the
91
+ * only one there is.
92
+ */
93
+ function readIdpMetadata(xml, entityId) {
94
+ const candidates = entitiesOf(xml).filter((entity) => section(entity.body, 'IDPSSODescriptor') !== undefined);
95
+ if (candidates.length === 0) {
96
+ throw new Error('the metadata has no IDPSSODescriptor: not an identity provider');
97
+ }
98
+ const entity = chooseEntity(candidates, entityId, 'identity provider', '--idp-entity-id');
99
+ const descriptor = section(entity.body, 'IDPSSODescriptor') ?? '';
100
+ const certificates = [];
101
+ const keyDescriptors = descriptor.matchAll(/<(?:[\w-]+:)?KeyDescriptor\b([^>]*)>([\s\S]*?)<\/(?:[\w-]+:)?KeyDescriptor>/g);
102
+ for (const [, attributes, body] of keyDescriptors) {
103
+ // No `use` means the key serves both purposes (SAML metadata 2.4.1.1).
104
+ if (attribute(attributes, 'use') === 'encryption')
105
+ continue;
106
+ for (const [, certificate] of body.matchAll(/<(?:[\w-]+:)?X509Certificate>([^<]+)</g)) {
107
+ certificates.push(certificate.replace(/\s+/g, ''));
108
+ }
109
+ }
110
+ const ssoServices = [
111
+ ...descriptor.matchAll(/<(?:[\w-]+:)?SingleSignOnService\b[^>]*>/g),
112
+ ].map(([tag]) => ({
113
+ binding: attribute(tag, 'Binding') ?? '',
114
+ location: attribute(tag, 'Location'),
115
+ }));
116
+ const ssoUrl = ssoServices.find((s) => s.binding.endsWith(':HTTP-Redirect'))?.location ??
117
+ ssoServices.find((s) => s.binding.endsWith(':HTTP-POST'))?.location;
118
+ return { entityId: entity.entityId, certificates, ssoUrl };
119
+ }
120
+ /** The bearer ACS of an entity: the one at `/oauth/token/alias/…`. */
121
+ function bearerAcsOf(body) {
122
+ return [...body.matchAll(/<(?:[\w-]+:)?AssertionConsumerService\b[^>]*>/g)]
123
+ .map(([tag]) => attribute(tag, 'Location'))
124
+ .find((location) => location?.includes('/oauth/token/alias/'));
125
+ }
126
+ /**
127
+ * The XSUAA service provider in `xml`: the entity publishing a bearer ACS,
128
+ * `spEntityId`'s when several do.
129
+ */
130
+ function readSpMetadata(xml, spEntityId) {
131
+ const candidates = entitiesOf(xml).filter((entity) => bearerAcsOf(entity.body));
132
+ if (candidates.length === 0)
133
+ return {};
134
+ const entity = chooseEntity(candidates, spEntityId, 'XSUAA service provider', '--sp-entity-id');
135
+ return { entityId: entity.entityId, bearerAcsUrl: bearerAcsOf(entity.body) };
136
+ }
137
+ /**
138
+ * Fills what the metadata states and the caller did not. An explicit option
139
+ * always wins, and trust is replaced rather than widened: a `--idp-cert`
140
+ * means no certificate from the metadata is trusted alongside it.
141
+ *
142
+ * `explicitTokenEndpoint` is the `--token-endpoint` the user gave, if any —
143
+ * distinct from `options.tokenEndpoint`, which a service key may already have
144
+ * set to the plain `/oauth/token` that a bearer grant must not use.
145
+ */
146
+ async function applySamlMetadata(options, explicitTokenEndpoint, load = loadMetadata) {
147
+ if (options.protocol !== 'saml2')
148
+ return;
149
+ if (options.idpMetadata) {
150
+ const idp = readIdpMetadata(await load(options.idpMetadata), options.idpEntityId);
151
+ options.idpEntityId ??= idp.entityId;
152
+ options.idpSsoUrl ??= idp.ssoUrl;
153
+ if (options.idpCertificates === undefined &&
154
+ options.idpCertificateFiles === undefined &&
155
+ idp.certificates.length > 0) {
156
+ options.idpCertificates = idp.certificates;
157
+ }
158
+ }
159
+ if (options.flow !== 'bearer')
160
+ return;
161
+ // The file wins over the service key's UAA, as an explicit source does.
162
+ const spSource = options.samlMetadataPath ??
163
+ (options.uaaUrl
164
+ ? `${options.uaaUrl.replace(/\/+$/, '')}/saml/metadata`
165
+ : undefined);
166
+ if (!spSource)
167
+ return;
168
+ const sp = readSpMetadata(await load(spSource), options.spEntityId);
169
+ if (!sp.bearerAcsUrl) {
170
+ throw new Error(`${spSource} names no /oauth/token/alias/ endpoint: not an XSUAA service provider's metadata`);
171
+ }
172
+ options.spEntityId ??= sp.entityId;
173
+ options.acsUrl ??= sp.bearerAcsUrl;
174
+ options.tokenEndpoint = explicitTokenEndpoint ?? sp.bearerAcsUrl;
175
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * A private working directory for one CLI run, removed however the run ends.
4
+ *
5
+ * The CLIs keep a temporary session store (and, for a `credentials`-wrapped
6
+ * service key, an unwrapped copy of the key) while they log in. Both hold the
7
+ * client secret. They used to live in `.tmp` beside the output file — in the
8
+ * user's sessions folder, shared by every run — and were removed only on
9
+ * success: a failed or interrupted login left the secret there, and two runs
10
+ * at once shared one directory that each removed on exit.
11
+ *
12
+ * Now each run gets its own directory under the OS temp dir, readable by the
13
+ * user alone, and it is removed on any exit: success, error, or a signal.
14
+ */
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.createWorkDir = createWorkDir;
50
+ const fs = __importStar(require("node:fs"));
51
+ const os = __importStar(require("node:os"));
52
+ const path = __importStar(require("node:path"));
53
+ const SIGNAL_EXIT_CODES = {
54
+ SIGINT: 130,
55
+ SIGTERM: 143,
56
+ SIGHUP: 129,
57
+ };
58
+ function createWorkDir(prefix) {
59
+ // mkdtemp creates the directory with mode 0700: the secret is the user's alone.
60
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
61
+ const remove = () => {
62
+ try {
63
+ fs.rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ catch {
66
+ // Nothing left to do on the way out.
67
+ }
68
+ };
69
+ // `exit` covers process.exit() and a normal end; it does not fire for a
70
+ // signal, so each is handled too, and exits with the conventional code.
71
+ process.once('exit', remove);
72
+ for (const [signal, code] of Object.entries(SIGNAL_EXIT_CODES)) {
73
+ process.once(signal, () => {
74
+ remove();
75
+ process.exit(code);
76
+ });
77
+ }
78
+ return dir;
79
+ }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * @mcp-abap-adt/auth-broker
3
- * JWT authentication broker for MCP ABAP ADT server
3
+ * Per-destination token broker: sessions and service keys from stores,
4
+ * tokens from an injected provider, results persisted.
4
5
  */
5
6
  export type { ITokenRefresher } from '@mcp-abap-adt/interfaces-auth';
6
7
  export type { AuthType } from '@mcp-abap-adt/interfaces-auth-sap';
7
8
  export type { ILogger } from '@mcp-abap-adt/interfaces-utils';
8
- export { AuthBroker, type AuthBrokerConfig } from './AuthBroker';
9
- export type { ITokenProvider, ITokenResult, TokenProviderOptions, } from './providers';
9
+ export { AuthBroker, type AuthBrokerConfig, type TokenProviderFactory, } from './AuthBroker';
10
+ export type { IRefreshableTokenProvider, ITokenProvider, ITokenResult, TokenProviderOptions, } from './providers';
10
11
  export type { IAuthorizationConfig, IConnectionConfig, IServiceKeyStore, ISessionStore, } from './stores/interfaces';
11
12
  export type { IConfig } from './types';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEjE,YAAY,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,YAAY,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAC9D,OAAO,EACL,UAAU,EACV,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,GAC1B,MAAM,cAAc,CAAC;AAEtB,YAAY,EACV,yBAAyB,EACzB,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  /**
3
3
  * @mcp-abap-adt/auth-broker
4
- * JWT authentication broker for MCP ABAP ADT server
4
+ * Per-destination token broker: sessions and service keys from stores,
5
+ * tokens from an injected provider, results persisted.
5
6
  */
6
7
  Object.defineProperty(exports, "__esModule", { value: true });
7
8
  exports.AuthBroker = void 0;
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Stateful providers handle token lifecycle internally (refresh/relogin).
5
5
  */
6
- import type { ITokenProvider, ITokenProviderOptions, ITokenResult } from '@mcp-abap-adt/interfaces-auth';
6
+ import type { IRefreshableTokenProvider, ITokenProvider, ITokenProviderOptions, ITokenResult } from '@mcp-abap-adt/interfaces-auth';
7
7
  import type { IAuthorizationConfig, IConnectionConfig } from '@mcp-abap-adt/interfaces-auth-sap';
8
- export type { IAuthorizationConfig, IConnectionConfig, ITokenProvider, ITokenResult, };
8
+ export type { IAuthorizationConfig, IConnectionConfig, IRefreshableTokenProvider, ITokenProvider, ITokenResult, };
9
9
  export type TokenProviderOptions = ITokenProviderOptions;
10
10
  //# sourceMappingURL=ITokenProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ITokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/ITokenProvider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,YAAY,EACb,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,mCAAmC,CAAC;AAG3C,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,YAAY,GACb,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAC"}
1
+ {"version":3,"file":"ITokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/ITokenProvider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,yBAAyB,EACzB,cAAc,EACd,qBAAqB,EACrB,YAAY,EACb,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,mCAAmC,CAAC;AAG3C,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,cAAc,EACd,YAAY,GACb,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,CAAC"}
@@ -4,5 +4,5 @@
4
4
  * Provider implementations are in separate packages:
5
5
  * - @mcp-abap-adt/auth-providers - XSUAA and BTP providers
6
6
  */
7
- export type { ITokenProvider, ITokenResult, TokenProviderOptions, } from './ITokenProvider';
7
+ export type { IRefreshableTokenProvider, ITokenProvider, ITokenResult, TokenProviderOptions, } from './ITokenProvider';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,yBAAyB,EACzB,cAAc,EACd,YAAY,EACZ,oBAAoB,GACrB,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-broker",
3
- "version": "2.2.0",
4
- "description": "JWT authentication broker for MCP ABAP ADT - manages tokens based on destination headers",
3
+ "version": "3.0.1",
4
+ "description": "Per-destination token broker for SAP BTP and ABAP: sessions and service keys from stores, tokens from an injected provider, results persisted - plus the mcp-auth and mcp-sso CLIs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
@@ -21,7 +21,11 @@
21
21
  "token",
22
22
  "broker",
23
23
  "mcp",
24
- "abap-adt"
24
+ "abap-adt",
25
+ "xsuaa",
26
+ "oauth2",
27
+ "saml",
28
+ "sso"
25
29
  ],
26
30
  "author": "Oleksii Kyslytsia <oleksij.kyslytsja@gmail.com>",
27
31
  "license": "LGPL-3.0-only",
@@ -60,13 +64,13 @@
60
64
  "mcp-sso": "./dist/bin/mcp-sso.js"
61
65
  },
62
66
  "engines": {
63
- "node": ">=18.2.0"
67
+ "node": "^22 || ^24"
64
68
  },
65
69
  "dependencies": {
66
- "@mcp-abap-adt/auth-providers": "^2.2.0",
67
- "@mcp-abap-adt/auth-stores": "^1.2.0",
68
- "@mcp-abap-adt/interfaces-auth": "^1.2.0",
69
- "@mcp-abap-adt/interfaces-auth-sap": "^1.0.0",
70
+ "@mcp-abap-adt/auth-providers": "^4.2.0",
71
+ "@mcp-abap-adt/auth-stores": "^1.2.3",
72
+ "@mcp-abap-adt/interfaces-auth": "^2.1.0",
73
+ "@mcp-abap-adt/interfaces-auth-sap": "^1.0.1",
70
74
  "@mcp-abap-adt/interfaces-utils": "^1.1.0",
71
75
  "axios": "^1.13.5"
72
76
  },
@@ -1,16 +0,0 @@
1
- /**
2
- * Formatting utilities for logging
3
- */
4
- /**
5
- * Format token for logging (start...end)
6
- * @param token Token string to format
7
- * @returns Formatted token string or undefined if token is empty
8
- */
9
- export declare function formatToken(token?: string): string | undefined;
10
- /**
11
- * Format timestamp to readable date/time string
12
- * @param timestamp Timestamp in milliseconds
13
- * @returns Formatted date string (e.g., "2025-12-25 19:21:27 UTC")
14
- */
15
- export declare function formatExpirationDate(timestamp: number): string;
16
- //# sourceMappingURL=formatting.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"formatting.d.ts","sourceRoot":"","sources":["../../src/utils/formatting.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI9D;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAS9D"}
@@ -1,34 +0,0 @@
1
- "use strict";
2
- /**
3
- * Formatting utilities for logging
4
- */
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.formatToken = formatToken;
7
- exports.formatExpirationDate = formatExpirationDate;
8
- /**
9
- * Format token for logging (start...end)
10
- * @param token Token string to format
11
- * @returns Formatted token string or undefined if token is empty
12
- */
13
- function formatToken(token) {
14
- if (!token)
15
- return undefined;
16
- if (token.length <= 50)
17
- return token;
18
- return `${token.substring(0, 25)}...${token.substring(token.length - 25)}`;
19
- }
20
- /**
21
- * Format timestamp to readable date/time string
22
- * @param timestamp Timestamp in milliseconds
23
- * @returns Formatted date string (e.g., "2025-12-25 19:21:27 UTC")
24
- */
25
- function formatExpirationDate(timestamp) {
26
- const date = new Date(timestamp);
27
- const year = date.getUTCFullYear();
28
- const month = String(date.getUTCMonth() + 1).padStart(2, '0');
29
- const day = String(date.getUTCDate()).padStart(2, '0');
30
- const hours = String(date.getUTCHours()).padStart(2, '0');
31
- const minutes = String(date.getUTCMinutes()).padStart(2, '0');
32
- const seconds = String(date.getUTCSeconds()).padStart(2, '0');
33
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
34
- }