@stalfh233/omc-cli 0.4.16 → 0.4.18
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/dist/bizservice-config.js +8 -3
- package/dist/cli.js +2 -0
- package/dist/closest-match.js +50 -0
- package/dist/config-store.js +4 -0
- package/dist/field-families.js +13 -2
- package/dist/goal-routes.js +25 -2
- package/dist/lanes/meta.js +1 -1
- package/dist/session-manager.js +14 -2
- package/dist/session.js +31 -2
- package/dist/token-cache.js +65 -0
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ToolRefusalError } from "./tool-types.js";
|
|
2
|
+
import { closestMatch } from "./closest-match.js";
|
|
2
3
|
/**
|
|
3
4
|
* Ticket 92 — business-integration write-face codec + preflight (pure functions,
|
|
4
5
|
* zero DSH/wire dependencies). Reused by service.register (92), service.update
|
|
@@ -159,9 +160,13 @@ function commonlistValue(serviceConfig, code) {
|
|
|
159
160
|
export function assertBizServiceAdapterWhitelisted(adapterCode) {
|
|
160
161
|
if (typeof adapterCode !== "string" || !adapterCode)
|
|
161
162
|
refuse(BIZSERVICE_CONFIG_BLOCKERS.adapterNotWhitelisted, "adapterCode 是必需的(结构化写面)", { adapterCode });
|
|
162
|
-
if (!BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode))
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
if (!BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode)) {
|
|
164
|
+
// F6: an off-by-one adapter guess gets a did-you-mean (same helper as the
|
|
165
|
+
// field families) before the prose.
|
|
166
|
+
const closest = closestMatch(adapterCode, BIZSERVICE_WRITE_ADAPTERS);
|
|
167
|
+
refuse(BIZSERVICE_CONFIG_BLOCKERS.adapterNotWhitelisted, `adapterCode ${adapterCode} 不在写面支持范围内;本写面仅支持 ${BIZSERVICE_WRITE_ADAPTERS.join("/")}。${closest !== undefined ? `你要找的可能是 ${closest}。` : ""}` +
|
|
168
|
+
`**自定义 Java 方法体的常规做法是:在扩展模块写一个接口,然后用 RESTFUL 适配器注册一个服务指向它**(service.register,adapterCode:RESTFUL,methodConfig.url 指向扩展端点);不要用 INTERNAL/custom_protocol 适配器(那是平台内部/自定义协议机制,不是二开接口的常规登记方式,也不是本工具的支持面)`, { adapterCode, supported: BIZSERVICE_WRITE_ADAPTERS, ...(closest !== undefined ? { closestMatch: closest } : {}) });
|
|
169
|
+
}
|
|
165
170
|
return adapterCode;
|
|
166
171
|
}
|
|
167
172
|
/** The AES boundary is not bypassable: a raw configJson string is refused before any wire call. */
|
package/dist/cli.js
CHANGED
|
@@ -185,6 +185,8 @@ async function cmdDoctor(store, environment, serviceCode) {
|
|
|
185
185
|
steps.push(`服务凭据 ok ${serviceCode} 鉴权参数形状正常(${probe.parameterCount} 项;掩码/占位/全 0 均未检出)`);
|
|
186
186
|
else
|
|
187
187
|
steps.push(`服务凭据 WARN ${serviceCode} ${probe.findings.length} 项问题(${probe.findings.map((finding) => `${finding.code}:${finding.valueKind}`).join("、")})——掩码/占位值不可用于真实调用;真值从任务环境事实取,不要抄 service.get 读回`);
|
|
188
|
+
if (serviceCode !== undefined)
|
|
189
|
+
steps.push(`连通性 -- 形状检查不能证明凭据有效;真实连通性用 method.test(平台侧发起调用)`);
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
catch (error) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F6 (r10-friction): the shared did-you-mean closest-match helper. Born from
|
|
3
|
+
* the live-measured field-family case (NUMBER → correct name NUMERICAL cost a
|
|
4
|
+
* full extra round); the same off-by-one guesses hit every enum-like gate
|
|
5
|
+
* (adapter codes, …). Case/underscore-insensitive, three acceptance shapes:
|
|
6
|
+
* prefix containment, small edit distance, shared-prefix + proportional
|
|
7
|
+
* distance. Ambiguous short inputs (TEXT, BOOL) intentionally match nothing —
|
|
8
|
+
* a misleading suggestion is worse than the plain list.
|
|
9
|
+
*/
|
|
10
|
+
export function closestMatch(input, candidates) {
|
|
11
|
+
const wanted = input.toUpperCase().replace(/[\s_-]/g, "");
|
|
12
|
+
if (wanted.length < 2)
|
|
13
|
+
return undefined;
|
|
14
|
+
let best;
|
|
15
|
+
for (const name of candidates) {
|
|
16
|
+
const candidate = name.toUpperCase().replace(/[\s_-]/g, "");
|
|
17
|
+
// 1. prefix containment (STAFF→STAFF_SELECTOR, DATETIME→DATE)
|
|
18
|
+
const contained = (candidate.startsWith(wanted) || wanted.startsWith(candidate)) && Math.min(wanted.length, candidate.length) >= 4;
|
|
19
|
+
// 2. small edit distance (NUMERIC→NUMERICAL, SHORTTEXT→SHORT_TEXT)
|
|
20
|
+
const distance = editDistance(wanted, candidate);
|
|
21
|
+
const nearMiss = distance <= 2;
|
|
22
|
+
// 3. shared prefix + proportional distance (NUMBER→NUMERICAL, the live-measured case)
|
|
23
|
+
const sharedPrefix = commonPrefixLength(wanted, candidate);
|
|
24
|
+
const proportional = sharedPrefix >= 3 && wanted.length >= 5 && distance / Math.max(wanted.length, candidate.length) <= 0.58;
|
|
25
|
+
const accepted = contained || nearMiss || proportional;
|
|
26
|
+
if (accepted && (!best || distance < best.distance))
|
|
27
|
+
best = { name, distance };
|
|
28
|
+
}
|
|
29
|
+
return best?.name;
|
|
30
|
+
}
|
|
31
|
+
function commonPrefixLength(a, b) {
|
|
32
|
+
let index = 0;
|
|
33
|
+
while (index < a.length && index < b.length && a[index] === b[index])
|
|
34
|
+
index++;
|
|
35
|
+
return index;
|
|
36
|
+
}
|
|
37
|
+
function editDistance(a, b) {
|
|
38
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
39
|
+
for (let i = 1; i <= a.length; i++) {
|
|
40
|
+
let previous = row[0];
|
|
41
|
+
row[0] = i;
|
|
42
|
+
for (let j = 1; j <= b.length; j++) {
|
|
43
|
+
const temp = row[j];
|
|
44
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
45
|
+
previous = temp;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return row[b.length];
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=closest-match.js.map
|
package/dist/config-store.js
CHANGED
|
@@ -30,6 +30,10 @@ export class ConfigStore {
|
|
|
30
30
|
constructor(paths = defaultPaths()) {
|
|
31
31
|
this.paths = paths;
|
|
32
32
|
}
|
|
33
|
+
/** Config root (config file's directory) — token-cache placement (F1). */
|
|
34
|
+
get configPaths() {
|
|
35
|
+
return this.paths;
|
|
36
|
+
}
|
|
33
37
|
atomicWrite(path, text, mode) {
|
|
34
38
|
mkdirSync(dirname(path), { recursive: true });
|
|
35
39
|
if (existsSync(path)) {
|
package/dist/field-families.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { closestMatch } from "./closest-match.js";
|
|
2
3
|
import { ToolRefusalError } from "./tool-types.js";
|
|
3
4
|
import { decodeJsonObjectColumn } from "./json-column.js";
|
|
4
5
|
import { FIELD_CONTROL_CODES, FIELD_DATE_DEFAULT_OPTIONS, FIELD_DATE_FORMATS, FIELD_FAMILY_LIST } from "./lanes/field-contract.js";
|
|
@@ -258,8 +259,18 @@ export function fieldTypeHint(type) {
|
|
|
258
259
|
for (const [concept, family] of Object.entries(FIELD_TYPE_SYNONYMS))
|
|
259
260
|
if (type.includes(concept) || concept.includes(type))
|
|
260
261
|
return `「${type}」看起来是概念名而非族名;最接近的是 ${family}`;
|
|
262
|
+
// F2 (r10-friction): an off-by-one family guess (NUMBER→NUMERICAL measured
|
|
263
|
+
// live as a full extra round) deserves a did-you-mean, not just the full
|
|
264
|
+
// list. Closest match by edit distance over the normalized names.
|
|
265
|
+
const closest = closestFamilyName(type);
|
|
266
|
+
if (closest)
|
|
267
|
+
return `「${type}」不是族名;你要找的可能是 ${closest}`;
|
|
261
268
|
return undefined;
|
|
262
269
|
}
|
|
270
|
+
/** Case/underscore-insensitive closest match over the family list (shared helper, see closest-match.ts). */
|
|
271
|
+
function closestFamilyName(type) {
|
|
272
|
+
return closestMatch(type, Object.keys(FIELD_PROPERTY_TYPES));
|
|
273
|
+
}
|
|
263
274
|
/** wire propertyType (int) -> form controlType (type-matrix.md §3.1). */
|
|
264
275
|
export const FIELD_CONTROL_TYPES = { ...CAPABILITY_CONTROL_TYPES };
|
|
265
276
|
export const SYSTEM_FIELD_CODES = new Set([
|
|
@@ -781,7 +792,7 @@ function choiceOptionsError(type, options) {
|
|
|
781
792
|
// No optionsSet: refuse the shapes that LOOK like choices but the platform ignores.
|
|
782
793
|
for (const key of ["items", "labels", "custom", "dictionary", "businessModel", "options", "dictionaryData"]) {
|
|
783
794
|
if (options[key] !== undefined)
|
|
784
|
-
return `选中控件(单选/复选/下拉)的选项必须放进 optionsSet
|
|
795
|
+
return `选中控件(单选/复选/下拉)的选项必须放进 optionsSet;裸的 options.${key} 平台不消费,门户会渲染成空控件。最小可抄样本 ${JSON.stringify({ options: { optionsSet: { optionsType: "custom", custom: [{ code: "opt1", value: "1" }, { code: "opt2", value: "2" }] } } })},或 field.create 直接传 choices:["opt1","opt2"]`;
|
|
785
796
|
}
|
|
786
797
|
return null;
|
|
787
798
|
}
|
|
@@ -805,7 +816,7 @@ export function assertFieldOptionsAllowed(type, options, schemaCode) {
|
|
|
805
816
|
refuseFieldOptions("FORMULA options carried the legacy top-level resultType/expression keys, which the platform does not read", "rejected: FORMULA(100) options must use formulaConfig (nested JSON string) + persistent:true; top-level resultType/expression are not platform keys");
|
|
806
817
|
const formulaConfig = options?.formulaConfig;
|
|
807
818
|
if (!isNonEmptyString(formulaConfig))
|
|
808
|
-
refuseFieldOptions("FORMULA options are missing the required formulaConfig JSON string", "rejected: FORMULA(100) requires options.formulaConfig (a nested JSON string: resultType/formulaType/expression)
|
|
819
|
+
refuseFieldOptions("FORMULA options are missing the required formulaConfig JSON string", "rejected: FORMULA(100) requires options.formulaConfig (a nested JSON string: resultType/formulaType/expression)。最小可抄样本 " + JSON.stringify({ options: { formulaConfig: JSON.stringify({ formulaType: "EXPRESSION", expression: "{amount}*2", resultType: 0 }), persistent: true } }) + "(resultType 0=SHORT_TEXT/2=NUMERICAL;expression 引用模型字段)");
|
|
809
820
|
const config = parseOptionsObject(formulaConfig);
|
|
810
821
|
if (!config)
|
|
811
822
|
refuseFieldOptions("FORMULA formulaConfig is not a parseable JSON object string", "rejected: FORMULA(100) options.formulaConfig must be a JSON object string");
|
package/dist/goal-routes.js
CHANGED
|
@@ -231,13 +231,36 @@ function batchReadCommands(pointers) {
|
|
|
231
231
|
byTopic.set(pointer.topic, [...(byTopic.get(pointer.topic) ?? []), pointer.section]);
|
|
232
232
|
return [...byTopic.entries()].map(([topic, sections]) => `omc help ${topic} ${sections.map((section) => `--section "${section}"`).join(" ")}`);
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* The route's methodology-read surface: pointers (+ per-section bytes) +
|
|
236
|
+
* ready-to-run batched commands + hint.
|
|
237
|
+
*
|
|
238
|
+
* F9 (r10-friction): the most common task flow was `next` followed
|
|
239
|
+
* IMMEDIATELY by fetching the first readSection — one extra process +
|
|
240
|
+
* round-trip every task. The first section now rides inline when it fits the
|
|
241
|
+
* inline budget (~2 KB); larger sections stay pointer-only so `next` never
|
|
242
|
+
* balloons.
|
|
243
|
+
*/
|
|
244
|
+
const FIRST_SECTION_INLINE_BUDGET_BYTES = 2560;
|
|
235
245
|
function routeReads(readSections) {
|
|
236
246
|
// Per-section UTF-8 bytes ride each pointer (one field, not a parallel array)
|
|
237
247
|
// so a caller can budget which sections to fetch — dogfood R1 found the
|
|
238
248
|
// batched readCommands could exceed a window with no way to see sizes.
|
|
239
249
|
const budgeted = readSections.map((pointer) => ({ ...pointer, bytes: sectionBytes(pointer) }));
|
|
240
|
-
|
|
250
|
+
// Inline the FIRST pointer that fits the budget (order = relevance); an
|
|
251
|
+
// oversized head section falls through to the next fitting one rather than
|
|
252
|
+
// dropping the inline content entirely.
|
|
253
|
+
let firstSection;
|
|
254
|
+
for (const pointer of budgeted.slice(0, 3)) {
|
|
255
|
+
if (pointer.bytes > 0 && pointer.bytes <= FIRST_SECTION_INLINE_BUDGET_BYTES) {
|
|
256
|
+
const resolved = readSkillSection(pointer.topic, pointer.section);
|
|
257
|
+
if (resolved) {
|
|
258
|
+
firstSection = { topic: pointer.topic, section: pointer.section, bytes: pointer.bytes, content: resolved.section };
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { readSections: budgeted, readCommands: batchReadCommands(readSections), readHint: READ_HINT, ...(firstSection !== undefined ? { firstSection } : {}) };
|
|
241
264
|
}
|
|
242
265
|
/** UTF-8 byte length of one methodology section (best-effort; 0 when unreadable). */
|
|
243
266
|
function sectionBytes(pointer) {
|
package/dist/lanes/meta.js
CHANGED
|
@@ -251,7 +251,7 @@ export function registerMetaLane(tools) {
|
|
|
251
251
|
? { step: "服务凭据", ok: null, serviceCode: probe.serviceCode, note: "未找到(get_by_code 无行)" }
|
|
252
252
|
: probe.status === "error"
|
|
253
253
|
? { step: "服务凭据", ok: null, serviceCode: probe.serviceCode, note: `探测失败:${probe.message}` }
|
|
254
|
-
: { step: "服务凭据", ok: probe.findings.length === 0, serviceCode: probe.serviceCode, adapterCode: probe.adapterCode, parameterCount: probe.parameterCount, findings: probe.findings, note: probe.findings.length === 0 ? "掩码/占位/全 0 均未检出" : "掩码/占位值不可用于真实调用;真值从任务环境事实取,不要抄 service.get 读回" });
|
|
254
|
+
: { step: "服务凭据", ok: probe.findings.length === 0, serviceCode: probe.serviceCode, adapterCode: probe.adapterCode, parameterCount: probe.parameterCount, findings: probe.findings, note: `${probe.findings.length === 0 ? "掩码/占位/全 0 均未检出" : "掩码/占位值不可用于真实调用;真值从任务环境事实取,不要抄 service.get 读回"};形状检查不能证明凭据有效,真实连通性用 method.test` });
|
|
255
255
|
}
|
|
256
256
|
return { status: "ok", steps };
|
|
257
257
|
}
|
package/dist/session-manager.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CloudPivotSession } from "./session.js";
|
|
1
|
+
import { CloudPivotSession, defaultRequestTimeoutMs } from "./session.js";
|
|
2
|
+
import { TokenCache, loginFingerprint } from "./token-cache.js";
|
|
2
3
|
/** Caches one logged-in session per named environment per process. */
|
|
3
4
|
export class SessionManager {
|
|
4
5
|
store;
|
|
@@ -20,7 +21,18 @@ export class SessionManager {
|
|
|
20
21
|
let session = cached?.profileKey === profileKey ? cached.session : undefined;
|
|
21
22
|
if (!session) {
|
|
22
23
|
const credential = this.store.readCredential(resolved.name);
|
|
23
|
-
|
|
24
|
+
// F1: cross-process token cache — lives beside the credentials, keyed
|
|
25
|
+
// by the environment profile + credential fingerprint. Injected http
|
|
26
|
+
// factories (tests, fakes) still go through the same hooks; a fake
|
|
27
|
+
// accepts the cached token the same way it accepts a fresh one.
|
|
28
|
+
const cache = new TokenCache(this.store.configPaths.configPath);
|
|
29
|
+
const environmentName = resolved.name;
|
|
30
|
+
const fingerprint = loginFingerprint(resolved.profile, credential);
|
|
31
|
+
session = new CloudPivotSession(resolved.name, resolved.profile, credential, this.httpFactory?.(resolved.profile), this.systemConfigFactory?.(resolved.profile), defaultRequestTimeoutMs(), {
|
|
32
|
+
load: () => cache.load(environmentName, fingerprint),
|
|
33
|
+
save: (login) => cache.save(environmentName, fingerprint, login),
|
|
34
|
+
clear: () => cache.clear(environmentName),
|
|
35
|
+
});
|
|
24
36
|
this.sessions.set(resolved.name, { profileKey, session });
|
|
25
37
|
}
|
|
26
38
|
await session.login();
|
package/dist/session.js
CHANGED
|
@@ -18,16 +18,18 @@ export class CloudPivotSession {
|
|
|
18
18
|
credential;
|
|
19
19
|
systemConfig;
|
|
20
20
|
requestTimeoutMs;
|
|
21
|
+
tokenCache;
|
|
21
22
|
loginResult;
|
|
22
23
|
version;
|
|
23
24
|
httpAdapter;
|
|
24
25
|
fingerprintOf;
|
|
25
|
-
constructor(environmentName, profile, credential, http, systemConfig = undefined, requestTimeoutMs = defaultRequestTimeoutMs()) {
|
|
26
|
+
constructor(environmentName, profile, credential, http, systemConfig = undefined, requestTimeoutMs = defaultRequestTimeoutMs(), tokenCache) {
|
|
26
27
|
this.environmentName = environmentName;
|
|
27
28
|
this.profile = profile;
|
|
28
29
|
this.credential = credential;
|
|
29
30
|
this.systemConfig = systemConfig;
|
|
30
31
|
this.requestTimeoutMs = requestTimeoutMs;
|
|
32
|
+
this.tokenCache = tokenCache;
|
|
31
33
|
this.httpAdapter = http;
|
|
32
34
|
}
|
|
33
35
|
async http() {
|
|
@@ -42,6 +44,24 @@ export class CloudPivotSession {
|
|
|
42
44
|
async login() {
|
|
43
45
|
if (this.loginResult)
|
|
44
46
|
return this.loginResult;
|
|
47
|
+
// F1: a fresh-enough cached login (same environment profile + credential
|
|
48
|
+
// fingerprint) skips the six-round-trip handshake entirely. A 401
|
|
49
|
+
// mid-call clears the cache and re-logins (request() below).
|
|
50
|
+
const cached = this.tokenCache?.load();
|
|
51
|
+
if (cached) {
|
|
52
|
+
const core = await omcCore();
|
|
53
|
+
if (!this.fingerprintOf)
|
|
54
|
+
this.fingerprintOf = core.canonicalCloudPivotEnvironmentFingerprint;
|
|
55
|
+
this.loginResult = {
|
|
56
|
+
token: cached.token,
|
|
57
|
+
enterpriseId: cached.enterpriseId,
|
|
58
|
+
administrator: cached.administrator,
|
|
59
|
+
...(cached.accountIdentity !== undefined ? { accountIdentity: cached.accountIdentity } : {}),
|
|
60
|
+
};
|
|
61
|
+
if (cached.version !== undefined)
|
|
62
|
+
this.version = cached.version;
|
|
63
|
+
return this.loginResult;
|
|
64
|
+
}
|
|
45
65
|
const core = await omcCore();
|
|
46
66
|
if (!this.fingerprintOf)
|
|
47
67
|
this.fingerprintOf = core.canonicalCloudPivotEnvironmentFingerprint;
|
|
@@ -59,6 +79,13 @@ export class CloudPivotSession {
|
|
|
59
79
|
...(systemConfig ? { systemConfig } : {}),
|
|
60
80
|
});
|
|
61
81
|
this.version = detectVersion(systemConfig);
|
|
82
|
+
this.tokenCache?.save({
|
|
83
|
+
token: this.loginResult.token,
|
|
84
|
+
enterpriseId: this.loginResult.enterpriseId,
|
|
85
|
+
administrator: this.loginResult.administrator,
|
|
86
|
+
...(this.loginResult.accountIdentity !== undefined ? { accountIdentity: this.loginResult.accountIdentity } : {}),
|
|
87
|
+
...(this.version !== undefined ? { version: this.version } : {}),
|
|
88
|
+
});
|
|
62
89
|
return this.loginResult;
|
|
63
90
|
}
|
|
64
91
|
/** Version-family evidence; absent evidence returns undefined (fail closed upstream). */
|
|
@@ -104,6 +131,8 @@ export class CloudPivotSession {
|
|
|
104
131
|
return first;
|
|
105
132
|
this.loginResult = undefined;
|
|
106
133
|
this.version = undefined;
|
|
134
|
+
// The invalidated token must not be re-loaded by the re-login below.
|
|
135
|
+
this.tokenCache?.clear();
|
|
107
136
|
return this.send(input);
|
|
108
137
|
}
|
|
109
138
|
async send(input) {
|
|
@@ -167,7 +196,7 @@ export class CloudPivotRequestTimeoutError extends Error {
|
|
|
167
196
|
this.name = "CloudPivotRequestTimeoutError";
|
|
168
197
|
}
|
|
169
198
|
}
|
|
170
|
-
function defaultRequestTimeoutMs() {
|
|
199
|
+
export function defaultRequestTimeoutMs() {
|
|
171
200
|
const raw = process.env.OMC_HTTP_TIMEOUT_MS;
|
|
172
201
|
const parsed = raw ? Number(raw) : NaN;
|
|
173
202
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 120_000;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
export function loginFingerprint(profile, credential) {
|
|
5
|
+
return createHash("sha256")
|
|
6
|
+
.update(JSON.stringify({ baseUrl: profile.baseUrl, applicationCode: profile.applicationCode, engineCode: profile.engineCode ?? null, username: credential.username, password: credential.password }))
|
|
7
|
+
.digest("hex");
|
|
8
|
+
}
|
|
9
|
+
const DEFAULT_TTL_MS = 8 * 60 * 60 * 1000;
|
|
10
|
+
export class TokenCache {
|
|
11
|
+
dir;
|
|
12
|
+
ttlMs;
|
|
13
|
+
constructor(configPath, ttlMs = Number(process.env.OMC_TOKEN_TTL_MS) > 0 ? Number(process.env.OMC_TOKEN_TTL_MS) : DEFAULT_TTL_MS) {
|
|
14
|
+
this.dir = join(dirname(configPath), "token-cache");
|
|
15
|
+
this.ttlMs = ttlMs;
|
|
16
|
+
}
|
|
17
|
+
load(environmentName, fingerprint) {
|
|
18
|
+
const file = join(this.dir, `${environmentName}.json`);
|
|
19
|
+
if (!existsSync(file))
|
|
20
|
+
return undefined;
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (parsed.fingerprint !== fingerprint)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (typeof parsed.storedAt !== "number" || Date.now() - parsed.storedAt > this.ttlMs)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (typeof parsed.token !== "string" || typeof parsed.enterpriseId !== "string" || typeof parsed.administrator !== "boolean")
|
|
33
|
+
return undefined;
|
|
34
|
+
return {
|
|
35
|
+
token: parsed.token,
|
|
36
|
+
enterpriseId: parsed.enterpriseId,
|
|
37
|
+
administrator: parsed.administrator,
|
|
38
|
+
...(parsed.accountIdentity !== undefined ? { accountIdentity: parsed.accountIdentity } : {}),
|
|
39
|
+
...(parsed.version !== undefined ? { version: parsed.version } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
save(environmentName, fingerprint, login) {
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
45
|
+
const entry = { ...login, fingerprint, storedAt: Date.now() };
|
|
46
|
+
const file = join(this.dir, `${environmentName}.json`);
|
|
47
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
48
|
+
writeFileSync(tmp, JSON.stringify(entry), { mode: 0o600 });
|
|
49
|
+
renameSync(tmp, file);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// The cache is an optimization, never a dependency: a failed write
|
|
53
|
+
// only means the next process logs in fresh.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
clear(environmentName) {
|
|
57
|
+
try {
|
|
58
|
+
rmSync(join(this.dir, `${environmentName}.json`), { force: true });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// best effort
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=token-cache.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"commander": "14.0.3",
|
|
54
|
-
"omc-core": "0.4.
|
|
54
|
+
"omc-core": "0.4.18",
|
|
55
55
|
"proper-lockfile": "4.1.2",
|
|
56
56
|
"write-file-atomic": "6.0.0",
|
|
57
57
|
"zod": "^3.23.8",
|