@contrail/flexplm 1.7.7-alpha.3e12cec → 1.7.7-alpha.522cd89
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/lib/interfaces/interfaces.d.ts +1 -13
- package/lib/publish/base-process-publish-assortment.js +4 -2
- package/lib/publish/base-process-publish-assortment.spec.js +106 -0
- package/lib/util/flexplm-connect.d.ts +4 -5
- package/lib/util/flexplm-connect.js +10 -133
- package/lib/util/flexplm-connect.spec.js +0 -617
- package/package.json +1 -1
- package/lib/util/flexplm-session-manager.d.ts +0 -40
- package/lib/util/flexplm-session-manager.js +0 -192
- package/lib/util/flexplm-session-manager.spec.d.ts +0 -1
- package/lib/util/flexplm-session-manager.spec.js +0 -70
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { FCConfig } from '../interfaces/interfaces';
|
|
2
|
-
export declare class SessionRejectedError extends Error {
|
|
3
|
-
}
|
|
4
|
-
/** Owns the FlexPLM session cookie cache: options, keying, expiry, in-flight acquisition sharing and Set-Cookie parsing. */
|
|
5
|
-
export declare class FlexPLMSessionManager {
|
|
6
|
-
private config;
|
|
7
|
-
constructor(config: FCConfig);
|
|
8
|
-
get options(): {
|
|
9
|
-
enabled: boolean;
|
|
10
|
-
ttlMs: number;
|
|
11
|
-
cookieName: string;
|
|
12
|
-
retryStatuses: number[];
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Waits until no cold session acquisition is running for this key.
|
|
16
|
-
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
17
|
-
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
18
|
-
*/
|
|
19
|
-
waitForInFlightAcquisition(): Promise<void> | undefined;
|
|
20
|
-
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
21
|
-
runColdAcquisition<T>(acquire: () => Promise<T>): Promise<T>;
|
|
22
|
-
private getSessionKey;
|
|
23
|
-
getCachedSession(): string | undefined;
|
|
24
|
-
/**
|
|
25
|
-
* Compare-and-set: stores `cookie` only if the cache still holds the cookie this request was sent with
|
|
26
|
-
* (`sentCookie`, undefined for a cold acquisition = nothing cached). A late response must not roll back or
|
|
27
|
-
* resurrect a session that a concurrent event already replaced or cleared.
|
|
28
|
-
* @returns true if the cache was updated
|
|
29
|
-
*/
|
|
30
|
-
storeSession(cookie: string, sentCookie: string | undefined): boolean;
|
|
31
|
-
clearSession(): void;
|
|
32
|
-
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
33
|
-
extractSessionCookie(response: Response): string | undefined;
|
|
34
|
-
/**
|
|
35
|
-
* Builds the Cookie header value from the configured static Cookie header and the managed session cookie.
|
|
36
|
-
* With session reuse enabled only the managed cookie is replaced/removed; every other static cookie pair is kept.
|
|
37
|
-
* With session reuse disabled the static value is returned unchanged.
|
|
38
|
-
*/
|
|
39
|
-
buildCookieHeader(staticCookie?: string, sessionCookie?: string): string | undefined;
|
|
40
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FlexPLMSessionManager = exports.SessionRejectedError = void 0;
|
|
4
|
-
const crypto_1 = require("crypto");
|
|
5
|
-
// Module-level on purpose: a new FlexPLMConnect (and session manager) is constructed for every event.
|
|
6
|
-
const sessionCache = new Map();
|
|
7
|
-
// Upper bound so users/option combinations that are never requested again cannot accumulate cookies forever.
|
|
8
|
-
const MAX_CACHED_SESSIONS = 100;
|
|
9
|
-
const pruneSessionCache = () => {
|
|
10
|
-
const now = Date.now();
|
|
11
|
-
for (const [key, session] of sessionCache) {
|
|
12
|
-
if (session.expiresAt <= now)
|
|
13
|
-
sessionCache.delete(key);
|
|
14
|
-
}
|
|
15
|
-
// Still full: drop the oldest entries (Map keeps insertion order).
|
|
16
|
-
while (sessionCache.size >= MAX_CACHED_SESSIONS) {
|
|
17
|
-
sessionCache.delete(sessionCache.keys().next().value);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
// In-flight cold session acquisitions per key, so concurrent events share one FlexPLM session instead of each creating their own.
|
|
21
|
-
const inFlightAcquisitions = new Map();
|
|
22
|
-
const SESSION_CACHE_DIGEST_KEY = (0, crypto_1.randomBytes)(32);
|
|
23
|
-
const DEFAULT_SESSION_COOKIE_NAME = 'JSESSIONID';
|
|
24
|
-
const DEFAULT_SESSION_TTL_SECONDS = 900;
|
|
25
|
-
const DEFAULT_RETRY_STATUSES = [401, 403];
|
|
26
|
-
const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
27
|
-
// Thrown internally when a cached cookie was rejected, so the caller can re-acquire under the lock.
|
|
28
|
-
class SessionRejectedError extends Error {
|
|
29
|
-
}
|
|
30
|
-
exports.SessionRejectedError = SessionRejectedError;
|
|
31
|
-
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
-
const parseRetryStatuses = (value) => {
|
|
33
|
-
let list = [];
|
|
34
|
-
if (Array.isArray(value))
|
|
35
|
-
list = value;
|
|
36
|
-
else if (typeof value === 'string')
|
|
37
|
-
list = value.split(',');
|
|
38
|
-
else if (value !== undefined && value !== null)
|
|
39
|
-
list = [value];
|
|
40
|
-
const statuses = list
|
|
41
|
-
.map(status => (typeof status === 'string' ? status.trim() : status))
|
|
42
|
-
.filter(status => status !== '' && status !== null && status !== undefined)
|
|
43
|
-
.map(Number)
|
|
44
|
-
// Only client-error statuses can mean "session rejected". 2xx/5xx would resend events the server already processed.
|
|
45
|
-
.filter(status => Number.isInteger(status) && status >= 400 && status <= 499);
|
|
46
|
-
return statuses.length ? statuses : DEFAULT_RETRY_STATUSES;
|
|
47
|
-
};
|
|
48
|
-
const parseTtlSeconds = (value) => {
|
|
49
|
-
const ttl = Number(value);
|
|
50
|
-
return Number.isFinite(ttl) && ttl > 0 ? ttl : DEFAULT_SESSION_TTL_SECONDS;
|
|
51
|
-
};
|
|
52
|
-
const parseCookieName = (value) => {
|
|
53
|
-
const name = typeof value === 'string' ? value.trim() : '';
|
|
54
|
-
return COOKIE_NAME_TOKEN.test(name) ? name : DEFAULT_SESSION_COOKIE_NAME;
|
|
55
|
-
};
|
|
56
|
-
/** Owns the FlexPLM session cookie cache: options, keying, expiry, in-flight acquisition sharing and Set-Cookie parsing. */
|
|
57
|
-
class FlexPLMSessionManager {
|
|
58
|
-
constructor(config) {
|
|
59
|
-
this.config = config;
|
|
60
|
-
}
|
|
61
|
-
get options() {
|
|
62
|
-
const sessionReuseConfig = this.config?.flexplmConnect?.sessionReuse ?? {};
|
|
63
|
-
return {
|
|
64
|
-
enabled: sessionReuseConfig.enabled === true || String(sessionReuseConfig.enabled).toLowerCase() === 'true',
|
|
65
|
-
ttlMs: parseTtlSeconds(sessionReuseConfig.ttlSeconds) * 1000,
|
|
66
|
-
cookieName: parseCookieName(sessionReuseConfig.cookieName),
|
|
67
|
-
retryStatuses: parseRetryStatuses(sessionReuseConfig.retryStatuses),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Waits until no cold session acquisition is running for this key.
|
|
72
|
-
* Callers must `await` this only when it returns a promise: awaiting even a resolved promise yields,
|
|
73
|
-
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
74
|
-
*/
|
|
75
|
-
waitForInFlightAcquisition() {
|
|
76
|
-
const key = this.getSessionKey();
|
|
77
|
-
if (!inFlightAcquisitions.has(key))
|
|
78
|
-
return undefined;
|
|
79
|
-
return (async () => {
|
|
80
|
-
let pending = inFlightAcquisitions.get(key);
|
|
81
|
-
while (pending) {
|
|
82
|
-
await pending.catch(() => undefined);
|
|
83
|
-
pending = inFlightAcquisitions.get(key);
|
|
84
|
-
}
|
|
85
|
-
})();
|
|
86
|
-
}
|
|
87
|
-
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
88
|
-
async runColdAcquisition(acquire) {
|
|
89
|
-
const key = this.getSessionKey();
|
|
90
|
-
const acquisition = acquire();
|
|
91
|
-
inFlightAcquisitions.set(key, acquisition);
|
|
92
|
-
try {
|
|
93
|
-
return await acquisition;
|
|
94
|
-
}
|
|
95
|
-
finally {
|
|
96
|
-
if (inFlightAcquisitions.get(key) === acquisition)
|
|
97
|
-
inFlightAcquisitions.delete(key);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
getSessionKey() {
|
|
101
|
-
// Options that change the cached value are part of the key; JSON avoids '|' collisions between fields.
|
|
102
|
-
const { cookieName, ttlMs } = this.options;
|
|
103
|
-
const staticHeaders = Object.entries(this.config.flexplmConnect?.staticHeaders ?? {})
|
|
104
|
-
.map(([name, value]) => [name.toLowerCase(), value])
|
|
105
|
-
.sort(([nameA, valueA], [nameB, valueB]) => {
|
|
106
|
-
if (nameA !== nameB)
|
|
107
|
-
return nameA < nameB ? -1 : 1;
|
|
108
|
-
return valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
|
|
109
|
-
});
|
|
110
|
-
const authenticationAndRoutingDigest = (0, crypto_1.createHmac)('sha256', SESSION_CACHE_DIGEST_KEY)
|
|
111
|
-
.update(JSON.stringify([this.config.userName(), this.config.password(), staticHeaders]))
|
|
112
|
-
.digest('hex');
|
|
113
|
-
return JSON.stringify([
|
|
114
|
-
this.config.apiHost, this.config.urlContext, this.config.plmEnviornment, cookieName, ttlMs, authenticationAndRoutingDigest,
|
|
115
|
-
]);
|
|
116
|
-
}
|
|
117
|
-
getCachedSession() {
|
|
118
|
-
if (!this.options.enabled)
|
|
119
|
-
return undefined;
|
|
120
|
-
const key = this.getSessionKey();
|
|
121
|
-
const cachedSessValue = sessionCache.get(key);
|
|
122
|
-
if (!cachedSessValue)
|
|
123
|
-
return undefined;
|
|
124
|
-
if (cachedSessValue.expiresAt <= Date.now()) {
|
|
125
|
-
sessionCache.delete(key);
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
return cachedSessValue.cookie;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Compare-and-set: stores `cookie` only if the cache still holds the cookie this request was sent with
|
|
132
|
-
* (`sentCookie`, undefined for a cold acquisition = nothing cached). A late response must not roll back or
|
|
133
|
-
* resurrect a session that a concurrent event already replaced or cleared.
|
|
134
|
-
* @returns true if the cache was updated
|
|
135
|
-
*/
|
|
136
|
-
storeSession(cookie, sentCookie) {
|
|
137
|
-
const key = this.getSessionKey();
|
|
138
|
-
if (sessionCache.get(key)?.cookie !== sentCookie)
|
|
139
|
-
return false;
|
|
140
|
-
// sliding expiry: Windchill's session timeout is also sliding
|
|
141
|
-
// delete first so a refreshed entry moves to the newest position (oldest-first eviction then acts as LRU)
|
|
142
|
-
sessionCache.delete(key);
|
|
143
|
-
pruneSessionCache();
|
|
144
|
-
sessionCache.set(key, { cookie, expiresAt: Date.now() + this.options.ttlMs });
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
clearSession() {
|
|
148
|
-
sessionCache.delete(this.getSessionKey());
|
|
149
|
-
}
|
|
150
|
-
/** Returns the session cookie value Windchill issued on this response, if any (last one wins). */
|
|
151
|
-
extractSessionCookie(response) {
|
|
152
|
-
const respHeader = response?.headers;
|
|
153
|
-
if (!respHeader)
|
|
154
|
-
return undefined;
|
|
155
|
-
let raw = [];
|
|
156
|
-
if (typeof respHeader.getSetCookie === 'function') {
|
|
157
|
-
raw = respHeader.getSetCookie();
|
|
158
|
-
}
|
|
159
|
-
else if (typeof respHeader.get === 'function' && respHeader.get('set-cookie')) {
|
|
160
|
-
raw = [respHeader.get('set-cookie')];
|
|
161
|
-
}
|
|
162
|
-
const name = this.options.cookieName;
|
|
163
|
-
const regexFilter = new RegExp('(?:^|[,\\s])' + escapeRegExp(name) + '=([^;,\\s]+)', 'g');
|
|
164
|
-
let jSession;
|
|
165
|
-
for (const line of raw) {
|
|
166
|
-
let cookieNameMatcher = regexFilter.exec(line);
|
|
167
|
-
while (cookieNameMatcher !== null) {
|
|
168
|
-
jSession = cookieNameMatcher[1];
|
|
169
|
-
cookieNameMatcher = regexFilter.exec(line);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return jSession;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Builds the Cookie header value from the configured static Cookie header and the managed session cookie.
|
|
176
|
-
* With session reuse enabled only the managed cookie is replaced/removed; every other static cookie pair is kept.
|
|
177
|
-
* With session reuse disabled the static value is returned unchanged.
|
|
178
|
-
*/
|
|
179
|
-
buildCookieHeader(staticCookie, sessionCookie) {
|
|
180
|
-
if (!this.options.enabled)
|
|
181
|
-
return staticCookie || undefined;
|
|
182
|
-
const name = this.options.cookieName;
|
|
183
|
-
const pairs = (staticCookie ?? '')
|
|
184
|
-
.split(';')
|
|
185
|
-
.map(pair => pair.trim())
|
|
186
|
-
.filter(pair => pair && pair.split('=')[0].trim() !== name);
|
|
187
|
-
if (sessionCookie)
|
|
188
|
-
pairs.push(`${name}=${sessionCookie}`);
|
|
189
|
-
return pairs.length ? pairs.join('; ') : undefined;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
exports.FlexPLMSessionManager = FlexPLMSessionManager;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const flexplm_session_manager_1 = require("./flexplm-session-manager");
|
|
4
|
-
let userCounter = 0;
|
|
5
|
-
const newManager = (user = 'mgr-user-' + (++userCounter), staticHeaders) => {
|
|
6
|
-
const config = {
|
|
7
|
-
apiHost: 'https://flexplm.example.com',
|
|
8
|
-
urlContext: '/FlexPLM',
|
|
9
|
-
vibeEventEndpoint: '/api/events',
|
|
10
|
-
csrfEndpoint: '/csrf',
|
|
11
|
-
plmEnviornment: 'test',
|
|
12
|
-
userName: () => user,
|
|
13
|
-
password: () => 'pass',
|
|
14
|
-
itemPreDevelopmentLifecycleStages: [],
|
|
15
|
-
flexplmConnect: { staticHeaders, sessionReuse: { enabled: true } },
|
|
16
|
-
};
|
|
17
|
-
return new flexplm_session_manager_1.FlexPLMSessionManager(config);
|
|
18
|
-
};
|
|
19
|
-
describe('FlexPLMSessionManager.storeSession (compare-and-set)', () => {
|
|
20
|
-
it('should store a cold acquisition when nothing is cached', () => {
|
|
21
|
-
const m = newManager();
|
|
22
|
-
expect(m.storeSession('a', undefined)).toBe(true);
|
|
23
|
-
expect(m.getCachedSession()).toBe('a');
|
|
24
|
-
});
|
|
25
|
-
it('should not let a cold acquisition overwrite an entry that appeared meanwhile', () => {
|
|
26
|
-
const m = newManager();
|
|
27
|
-
m.storeSession('a', undefined);
|
|
28
|
-
expect(m.storeSession('b', undefined)).toBe(false);
|
|
29
|
-
expect(m.getCachedSession()).toBe('a');
|
|
30
|
-
});
|
|
31
|
-
it('should replace or refresh the entry while it still holds the cookie that was sent', () => {
|
|
32
|
-
const m = newManager();
|
|
33
|
-
m.storeSession('old', undefined);
|
|
34
|
-
expect(m.storeSession('new', 'old')).toBe(true);
|
|
35
|
-
expect(m.getCachedSession()).toBe('new');
|
|
36
|
-
expect(m.storeSession('new', 'new')).toBe(true); // sliding refresh with the same cookie
|
|
37
|
-
});
|
|
38
|
-
it('should ignore a late response that was sent with an older cookie', () => {
|
|
39
|
-
const m = newManager();
|
|
40
|
-
m.storeSession('old', undefined);
|
|
41
|
-
m.storeSession('new', 'old');
|
|
42
|
-
expect(m.storeSession('old', 'old')).toBe(false);
|
|
43
|
-
expect(m.getCachedSession()).toBe('new');
|
|
44
|
-
});
|
|
45
|
-
it('should not resurrect a session that was cleared meanwhile', () => {
|
|
46
|
-
const m = newManager();
|
|
47
|
-
m.storeSession('old', undefined);
|
|
48
|
-
m.clearSession();
|
|
49
|
-
expect(m.storeSession('old', 'old')).toBe(false);
|
|
50
|
-
expect(m.getCachedSession()).toBeUndefined();
|
|
51
|
-
});
|
|
52
|
-
it('should isolate cached sessions when static authorization headers differ', () => {
|
|
53
|
-
const firstManager = newManager('shared-user', { Authorization: 'Basic first', PLM_ENV: 'test' });
|
|
54
|
-
const secondManager = newManager('shared-user', { Authorization: 'Basic second', PLM_ENV: 'test' });
|
|
55
|
-
firstManager.storeSession('first-session', undefined);
|
|
56
|
-
expect(secondManager.getCachedSession()).toBeUndefined();
|
|
57
|
-
});
|
|
58
|
-
it('should isolate cached sessions when static routing headers differ', () => {
|
|
59
|
-
const firstManager = newManager('shared-user', { 'X-Route': 'route-a' });
|
|
60
|
-
const secondManager = newManager('shared-user', { 'X-Route': 'route-b' });
|
|
61
|
-
firstManager.storeSession('first-session', undefined);
|
|
62
|
-
expect(secondManager.getCachedSession()).toBeUndefined();
|
|
63
|
-
});
|
|
64
|
-
it('should reuse cached sessions for equivalent static headers regardless of header order or casing', () => {
|
|
65
|
-
const firstManager = newManager('shared-user', { 'Authorization': 'Basic same', 'X-Route': 'route-a' });
|
|
66
|
-
const secondManager = newManager('shared-user', { 'x-route': 'route-a', 'authorization': 'Basic same' });
|
|
67
|
-
firstManager.storeSession('shared-session', undefined);
|
|
68
|
-
expect(secondManager.getCachedSession()).toBe('shared-session');
|
|
69
|
-
});
|
|
70
|
-
});
|