pi-harness-runtime 1.1.158 → 1.1.159
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/harness/infisical-config.ts +395 -0
- package/harness/infisical-setup.ts +270 -0
- package/harness/key-loader.ts +129 -29
- package/harness/secret-provider.ts +437 -0
- package/harness/secret-schema.ts +163 -0
- package/harness/varlock-worker.ts +379 -0
- package/index.ts +12 -3
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
package/harness/key-loader.ts
CHANGED
|
@@ -1,84 +1,184 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Key Loader —
|
|
2
|
+
* Key Loader — Unified credential loading
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* loadKeys(); // Loads all keys into process.env
|
|
4
|
+
* Loads API keys from Infisical (via Varlock) with file fallback.
|
|
5
|
+
* Priority: Infisical > Environment > Key Files
|
|
7
6
|
*
|
|
8
7
|
* Environment variables set:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Jev: TYPESAFE_API_KEY, OPENROUTER_API_KEY
|
|
9
|
+
* Telegram: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_WEBHOOK_SECRET
|
|
10
|
+
* Honcho: HONCHO_API_KEY
|
|
11
|
+
* Qdrant: QDRANT_CLUSTER_ENDPOINT, QDRANT_API_KEY, QDRANT_COLLECTION
|
|
12
|
+
* Embeddings: OPENAI_API_KEY
|
|
13
|
+
* Planners: PLANNER_API_KEY, PLANNER_BASE_URL, PLANNER_MODEL
|
|
14
|
+
* GLM_API_KEY, GLM_BASE_URL, GLM_MODEL
|
|
15
|
+
* MINIMAX_API_KEY, MINIMAX_BASE_URL, MINIMAX_MODEL
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
16
19
|
import { join } from "node:path";
|
|
17
20
|
import { homedir } from "node:os";
|
|
21
|
+
import { isConfigured, loadConfig } from "./infisical-config.js";
|
|
22
|
+
import { getSecretProvider, SecretProvider } from "./secret-provider.js";
|
|
18
23
|
|
|
19
24
|
const KEYS_DIR = join(homedir(), ".pi-harness-runtime", "keys");
|
|
20
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Key file mapping
|
|
28
|
+
*/
|
|
21
29
|
const KEY_FILE_MAP: Record<string, string> = {
|
|
22
|
-
//
|
|
30
|
+
// Jev keys
|
|
31
|
+
TYPESAFE_API_KEY: "typesafe-api-key.txt",
|
|
32
|
+
OPENROUTER_API_KEY: "openrouter-api-key.txt",
|
|
33
|
+
|
|
34
|
+
// Telegram keys
|
|
23
35
|
TELEGRAM_BOT_TOKEN: "telegram-bot-token.txt",
|
|
24
36
|
TELEGRAM_CHAT_ID: "telegram-chat-id.txt",
|
|
25
37
|
TELEGRAM_WEBHOOK_URL: "telegram-webhook-url.txt",
|
|
26
38
|
TELEGRAM_WEBHOOK_SECRET: "telegram-webhook-secret.txt",
|
|
27
|
-
|
|
39
|
+
|
|
40
|
+
// Honcho keys
|
|
41
|
+
HONCHO_API_KEY: "honcho-api-key.txt",
|
|
42
|
+
|
|
43
|
+
// Qdrant keys
|
|
44
|
+
QDRANT_CLUSTER_ENDPOINT: "qdrant-cluster-endpoint.txt",
|
|
45
|
+
QDRANT_API_KEY: "qdrant-api-key.txt",
|
|
46
|
+
QDRANT_COLLECTION: "qdrant-collection.txt",
|
|
47
|
+
|
|
48
|
+
// Embeddings
|
|
49
|
+
OPENAI_API_KEY: "openai-api-key.txt",
|
|
50
|
+
|
|
51
|
+
// Planner keys
|
|
28
52
|
PLANNER_API_KEY: "planner-api-key.txt",
|
|
29
53
|
PLANNER_MODEL: "planner-model.txt",
|
|
30
54
|
PLANNER_BASE_URL: "planner-base-url.txt",
|
|
55
|
+
|
|
56
|
+
// GLM keys
|
|
31
57
|
GLM_API_KEY: "glm-api-key.txt",
|
|
32
58
|
GLM_MODEL: "glm-model.txt",
|
|
33
59
|
GLM_BASE_URL: "glm-base-url.txt",
|
|
60
|
+
|
|
61
|
+
// Minimax keys
|
|
34
62
|
MINIMAX_API_KEY: "minimax-api-key.txt",
|
|
35
63
|
MINIMAX_MODEL: "minimax-model.txt",
|
|
36
64
|
MINIMAX_BASE_URL: "minimax-base-url.txt",
|
|
37
65
|
};
|
|
38
66
|
|
|
39
67
|
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
68
|
+
* Try to get from Infisical provider
|
|
69
|
+
*/
|
|
70
|
+
function getFromProvider(key: string): string | undefined {
|
|
71
|
+
try {
|
|
72
|
+
if (!isConfigured()) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const provider = getSecretProvider();
|
|
77
|
+
if (!provider.isReady()) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return provider.get(key);
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load a single key with precedence: Infisical > Environment > File
|
|
89
|
+
*/
|
|
90
|
+
function loadKey(key: string, filename: string): string | undefined {
|
|
91
|
+
// 1. Check if already in environment
|
|
92
|
+
if (process.env[key]) {
|
|
93
|
+
return process.env[key];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. Check Infisical provider
|
|
97
|
+
const providerValue = getFromProvider(key);
|
|
98
|
+
if (providerValue) {
|
|
99
|
+
return providerValue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Fall back to file
|
|
103
|
+
const filePath = join(KEYS_DIR, filename);
|
|
104
|
+
if (existsSync(filePath)) {
|
|
105
|
+
const value = readFileSync(filePath, "utf8").trim();
|
|
106
|
+
if (value) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Load all keys into process.env
|
|
116
|
+
* Only sets env vars that are not already set
|
|
42
117
|
*/
|
|
43
118
|
export function loadKeys(): void {
|
|
44
119
|
if (!existsSync(KEYS_DIR)) {
|
|
45
|
-
console.
|
|
46
|
-
return;
|
|
120
|
+
console.log(`[key-loader] Keys directory not found: ${KEYS_DIR}`);
|
|
47
121
|
}
|
|
48
122
|
|
|
49
123
|
let loadedCount = 0;
|
|
124
|
+
let infisicalCount = 0;
|
|
125
|
+
let fileCount = 0;
|
|
126
|
+
|
|
50
127
|
for (const [envVar, filename] of Object.entries(KEY_FILE_MAP)) {
|
|
51
|
-
// Skip if already set
|
|
128
|
+
// Skip if already set
|
|
52
129
|
if (process.env[envVar]) {
|
|
53
|
-
console.log(`[key-loader] ${envVar} already set via env, skipping`);
|
|
54
130
|
continue;
|
|
55
131
|
}
|
|
56
132
|
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
133
|
+
const value = loadKey(envVar, filename);
|
|
134
|
+
if (value) {
|
|
135
|
+
process.env[envVar] = value;
|
|
136
|
+
loadedCount++;
|
|
137
|
+
|
|
138
|
+
// Track source
|
|
139
|
+
if (getFromProvider(envVar)) {
|
|
140
|
+
infisicalCount++;
|
|
141
|
+
} else {
|
|
142
|
+
fileCount++;
|
|
64
143
|
}
|
|
65
144
|
}
|
|
66
145
|
}
|
|
67
146
|
|
|
68
147
|
if (loadedCount > 0) {
|
|
69
|
-
|
|
148
|
+
const source = infisicalCount > 0 ? `Infisical(${infisicalCount})` : "";
|
|
149
|
+
const sourceParts = [source, fileCount > 0 ? `Files(${fileCount})` : ""].filter(Boolean);
|
|
150
|
+
console.log(`[key-loader] Loaded ${loadedCount} key(s): ${sourceParts.join(", ")}`);
|
|
70
151
|
}
|
|
71
152
|
}
|
|
72
153
|
|
|
73
154
|
/**
|
|
74
|
-
* Get a specific key by name
|
|
155
|
+
* Get a specific key by name
|
|
75
156
|
*/
|
|
76
157
|
export function getKey(keyName: string): string | undefined {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
return
|
|
158
|
+
const filename = KEY_FILE_MAP[keyName];
|
|
159
|
+
if (!filename) {
|
|
160
|
+
return undefined;
|
|
80
161
|
}
|
|
81
|
-
return
|
|
162
|
+
return loadKey(keyName, filename);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if keys directory exists
|
|
167
|
+
*/
|
|
168
|
+
export function hasKeysDir(): boolean {
|
|
169
|
+
return existsSync(KEYS_DIR);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* List available key files
|
|
174
|
+
*/
|
|
175
|
+
export function listKeyFiles(): string[] {
|
|
176
|
+
if (!existsSync(KEYS_DIR)) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { readdirSync } = require("node:fs");
|
|
181
|
+
return readdirSync(KEYS_DIR).filter((f: string) => f.endsWith(".txt"));
|
|
82
182
|
}
|
|
83
183
|
|
|
84
184
|
export default loadKeys;
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret Provider Module
|
|
3
|
+
*
|
|
4
|
+
* Shared async resolution, immutable snapshots, and precedence handling.
|
|
5
|
+
* All services await this provider for credentials.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
InfisicalConfig,
|
|
10
|
+
loadConfig,
|
|
11
|
+
isConfigured,
|
|
12
|
+
activateRevision,
|
|
13
|
+
createRevision,
|
|
14
|
+
getSchemaPath,
|
|
15
|
+
getRevision,
|
|
16
|
+
markRevisionValidated,
|
|
17
|
+
getSiteUrl,
|
|
18
|
+
ensureDirectories,
|
|
19
|
+
} from "./infisical-config.js";
|
|
20
|
+
import {
|
|
21
|
+
resolveSecrets,
|
|
22
|
+
ResolutionResult,
|
|
23
|
+
getVarlockVersion,
|
|
24
|
+
validateSchema,
|
|
25
|
+
} from "./varlock-worker.js";
|
|
26
|
+
import {
|
|
27
|
+
generateSchema,
|
|
28
|
+
ServiceSecretMapping,
|
|
29
|
+
ServiceName,
|
|
30
|
+
} from "./secret-schema.js";
|
|
31
|
+
import { logServiceEvent } from "./service-diagnostics.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Immutable credential snapshot
|
|
35
|
+
*/
|
|
36
|
+
export interface CredentialSnapshot {
|
|
37
|
+
revisionId: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
secrets: Record<string, string>; // key -> value
|
|
40
|
+
sources: Record<string, "infisical" | "env" | "file">; // key -> source
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Provider status
|
|
45
|
+
*/
|
|
46
|
+
export type ProviderStatus =
|
|
47
|
+
| "unconfigured"
|
|
48
|
+
| "validating"
|
|
49
|
+
| "resolved"
|
|
50
|
+
| "saved_awaiting_restart"
|
|
51
|
+
| "active"
|
|
52
|
+
| "degraded";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Provider state
|
|
56
|
+
*/
|
|
57
|
+
export interface ProviderState {
|
|
58
|
+
status: ProviderStatus;
|
|
59
|
+
config: InfisicalConfig | null;
|
|
60
|
+
currentSnapshot: CredentialSnapshot | null;
|
|
61
|
+
activeRevision: string | null;
|
|
62
|
+
lastResolvedAt: string | null;
|
|
63
|
+
lastError: string | null;
|
|
64
|
+
errorCode: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Secret Provider singleton
|
|
69
|
+
*/
|
|
70
|
+
export class SecretProvider {
|
|
71
|
+
private static instance: SecretProvider | null = null;
|
|
72
|
+
|
|
73
|
+
private state: ProviderState = {
|
|
74
|
+
status: "unconfigured",
|
|
75
|
+
config: null,
|
|
76
|
+
currentSnapshot: null,
|
|
77
|
+
activeRevision: null,
|
|
78
|
+
lastResolvedAt: null,
|
|
79
|
+
lastError: null,
|
|
80
|
+
errorCode: null,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
private resolutionPromise: Promise<void> | null = null;
|
|
84
|
+
private abortController: AbortController | null = null;
|
|
85
|
+
|
|
86
|
+
private constructor() {}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get singleton instance
|
|
90
|
+
*/
|
|
91
|
+
static getInstance(): SecretProvider {
|
|
92
|
+
if (!SecretProvider.instance) {
|
|
93
|
+
SecretProvider.instance = new SecretProvider();
|
|
94
|
+
}
|
|
95
|
+
return SecretProvider.instance;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Initialize provider from config
|
|
100
|
+
*/
|
|
101
|
+
async initialize(): Promise<void> {
|
|
102
|
+
const config = loadConfig();
|
|
103
|
+
|
|
104
|
+
if (!config || !config.enabled) {
|
|
105
|
+
this.state.status = "unconfigured";
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.state.config = config;
|
|
110
|
+
this.state.activeRevision = config.activeRevision;
|
|
111
|
+
|
|
112
|
+
// Load current snapshot if we have an active revision
|
|
113
|
+
if (config.activeRevision) {
|
|
114
|
+
const snapshot = await this.loadSnapshot(config.activeRevision);
|
|
115
|
+
if (snapshot) {
|
|
116
|
+
this.state.currentSnapshot = snapshot;
|
|
117
|
+
this.state.status = "active";
|
|
118
|
+
this.state.lastResolvedAt = config.lastResolvedAt;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if provider is ready
|
|
125
|
+
*/
|
|
126
|
+
isReady(): boolean {
|
|
127
|
+
return this.state.status === "active" && this.state.currentSnapshot !== null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get current status
|
|
132
|
+
*/
|
|
133
|
+
getStatus(): ProviderState {
|
|
134
|
+
return { ...this.state };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get a secret value
|
|
139
|
+
*/
|
|
140
|
+
get(key: string): string | undefined {
|
|
141
|
+
if (!this.state.currentSnapshot) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return this.state.currentSnapshot.secrets[key];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get all secrets for a service
|
|
149
|
+
*/
|
|
150
|
+
getServiceSecrets(service: ServiceName): Record<string, string> {
|
|
151
|
+
const result: Record<string, string> = {};
|
|
152
|
+
|
|
153
|
+
if (!this.state.currentSnapshot || !this.state.config) {
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const mapping = this.state.config.services.find(s => s.service === service);
|
|
158
|
+
if (!mapping) {
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const [localKey, secretName] of Object.entries(mapping.keys)) {
|
|
163
|
+
const value = this.state.currentSnapshot.secrets[secretName];
|
|
164
|
+
if (value !== undefined) {
|
|
165
|
+
result[localKey] = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Refresh secrets (resolve new snapshot)
|
|
174
|
+
*/
|
|
175
|
+
async refresh(): Promise<{ success: boolean; error?: string }> {
|
|
176
|
+
// Don't allow concurrent refresh
|
|
177
|
+
if (this.resolutionPromise) {
|
|
178
|
+
return { success: false, error: "Refresh already in progress" };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const config = loadConfig();
|
|
182
|
+
if (!config || !config.enabled) {
|
|
183
|
+
return { success: false, error: "Infisical not configured" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.abortController = new AbortController();
|
|
187
|
+
this.state.status = "validating";
|
|
188
|
+
|
|
189
|
+
logServiceEvent({
|
|
190
|
+
ts: new Date().toISOString(),
|
|
191
|
+
level: "info",
|
|
192
|
+
service: "infisical",
|
|
193
|
+
operation: "refresh_started",
|
|
194
|
+
phase: "start",
|
|
195
|
+
projectId: config.projectId,
|
|
196
|
+
environment: config.environment,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.resolutionPromise = this.doRefresh(config, this.abortController.signal);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await this.resolutionPromise;
|
|
203
|
+
return { success: true };
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
error: err instanceof Error ? err.message : String(err),
|
|
208
|
+
};
|
|
209
|
+
} finally {
|
|
210
|
+
this.resolutionPromise = null;
|
|
211
|
+
this.abortController = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Internal refresh implementation
|
|
217
|
+
*/
|
|
218
|
+
private async doRefresh(
|
|
219
|
+
config: InfisicalConfig,
|
|
220
|
+
signal: AbortSignal
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
try {
|
|
223
|
+
// Generate schema for this revision
|
|
224
|
+
const revisionId = createRevision(config);
|
|
225
|
+
const schemaPath = getSchemaPath(revisionId);
|
|
226
|
+
|
|
227
|
+
const schema = generateSchema(config);
|
|
228
|
+
ensureDirectories();
|
|
229
|
+
|
|
230
|
+
// Write schema with proper permissions
|
|
231
|
+
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
232
|
+
writeFileSync(schemaPath, schema, { mode: 0o600 });
|
|
233
|
+
chmodSync(schemaPath, 0o600);
|
|
234
|
+
|
|
235
|
+
// Validate schema
|
|
236
|
+
const validation = await validateSchema(schemaPath);
|
|
237
|
+
if (!validation.valid) {
|
|
238
|
+
logServiceEvent({
|
|
239
|
+
ts: new Date().toISOString(),
|
|
240
|
+
level: "error",
|
|
241
|
+
service: "infisical",
|
|
242
|
+
operation: "schema_validation_failed",
|
|
243
|
+
phase: "failed",
|
|
244
|
+
error: validation.error,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.state.status = "degraded";
|
|
248
|
+
this.state.lastError = validation.error ?? "Schema validation failed";
|
|
249
|
+
this.state.errorCode = "validation";
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Resolve secrets
|
|
254
|
+
const bootstrapEnv = this.getBootstrapEnv(config);
|
|
255
|
+
const result = await resolveSecrets(
|
|
256
|
+
{
|
|
257
|
+
schemaPath,
|
|
258
|
+
cwd: process.cwd(),
|
|
259
|
+
env: bootstrapEnv,
|
|
260
|
+
skipCache: true,
|
|
261
|
+
},
|
|
262
|
+
signal
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (!result.success) {
|
|
266
|
+
logServiceEvent({
|
|
267
|
+
ts: new Date().toISOString(),
|
|
268
|
+
level: "error",
|
|
269
|
+
service: "infisical",
|
|
270
|
+
operation: "secrets_resolve_failed",
|
|
271
|
+
phase: "failed",
|
|
272
|
+
errorCode: result.errorCode,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
this.state.status = "degraded";
|
|
276
|
+
this.state.lastError = result.error ?? null;
|
|
277
|
+
this.state.errorCode = result.errorCode ?? "unknown";
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Build snapshot
|
|
282
|
+
const secrets: Record<string, string> = {};
|
|
283
|
+
const sources: Record<string, "infisical" | "env" | "file"> = {};
|
|
284
|
+
|
|
285
|
+
for (const secret of result.secrets ?? []) {
|
|
286
|
+
secrets[secret.key] = secret.value;
|
|
287
|
+
sources[secret.key] = "infisical";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const snapshot: CredentialSnapshot = {
|
|
291
|
+
revisionId,
|
|
292
|
+
createdAt: new Date().toISOString(),
|
|
293
|
+
secrets,
|
|
294
|
+
sources,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Save snapshot
|
|
298
|
+
await this.saveSnapshot(revisionId, snapshot);
|
|
299
|
+
markRevisionValidated(revisionId);
|
|
300
|
+
|
|
301
|
+
// Activate revision
|
|
302
|
+
activateRevision(revisionId);
|
|
303
|
+
|
|
304
|
+
// Update state
|
|
305
|
+
this.state.currentSnapshot = snapshot;
|
|
306
|
+
this.state.activeRevision = revisionId;
|
|
307
|
+
this.state.status = "active";
|
|
308
|
+
this.state.lastResolvedAt = new Date().toISOString();
|
|
309
|
+
this.state.lastError = null;
|
|
310
|
+
this.state.errorCode = null;
|
|
311
|
+
|
|
312
|
+
logServiceEvent({
|
|
313
|
+
ts: new Date().toISOString(),
|
|
314
|
+
level: "info",
|
|
315
|
+
service: "infisical",
|
|
316
|
+
operation: "snapshot_activated",
|
|
317
|
+
phase: "completed",
|
|
318
|
+
revisionId,
|
|
319
|
+
secretCount: result.secrets?.length ?? 0,
|
|
320
|
+
});
|
|
321
|
+
} catch (err) {
|
|
322
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
323
|
+
this.state.status = "degraded";
|
|
324
|
+
this.state.lastError = "Cancelled";
|
|
325
|
+
this.state.errorCode = "cancelled";
|
|
326
|
+
} else {
|
|
327
|
+
this.state.status = "degraded";
|
|
328
|
+
this.state.lastError = err instanceof Error ? err.message : String(err);
|
|
329
|
+
this.state.errorCode = "unknown";
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get bootstrap environment for Varlock
|
|
336
|
+
*/
|
|
337
|
+
private getBootstrapEnv(config: InfisicalConfig): Record<string, string> {
|
|
338
|
+
const env: Record<string, string> = {};
|
|
339
|
+
|
|
340
|
+
if (config.bootstrapRef.type === "env") {
|
|
341
|
+
const value = process.env[config.bootstrapRef.name];
|
|
342
|
+
if (value) {
|
|
343
|
+
env[config.bootstrapRef.name] = value;
|
|
344
|
+
}
|
|
345
|
+
} else if (config.bootstrapRef.type === "file") {
|
|
346
|
+
// Read bootstrap from file
|
|
347
|
+
try {
|
|
348
|
+
const { readFileSync } = require("node:fs");
|
|
349
|
+
const content = readFileSync(config.bootstrapRef.path, "utf8").trim();
|
|
350
|
+
// Parse KEY=value lines
|
|
351
|
+
for (const line of content.split("\n")) {
|
|
352
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
353
|
+
if (match) {
|
|
354
|
+
env[match[1]!.trim()] = match[2]!.trim();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// Bootstrap file not accessible
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return env;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Load snapshot from disk
|
|
367
|
+
*/
|
|
368
|
+
private async loadSnapshot(revisionId: string): Promise<CredentialSnapshot | null> {
|
|
369
|
+
const { loadConfig: reloadConfig } = await import("./infisical-config.js");
|
|
370
|
+
const { existsSync, readFileSync } = await import("node:fs");
|
|
371
|
+
const { join } = await import("node:path");
|
|
372
|
+
|
|
373
|
+
const snapshotPath = join(
|
|
374
|
+
(await import("./infisical-config.js")).getRevisionsDir(),
|
|
375
|
+
revisionId,
|
|
376
|
+
"snapshot.json"
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if (!existsSync(snapshotPath)) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const content = readFileSync(snapshotPath, "utf8");
|
|
385
|
+
return JSON.parse(content) as CredentialSnapshot;
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Save snapshot to disk
|
|
393
|
+
*/
|
|
394
|
+
private async saveSnapshot(revisionId: string, snapshot: CredentialSnapshot): Promise<void> {
|
|
395
|
+
const { getRevisionsDir } = await import("./infisical-config.js");
|
|
396
|
+
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
397
|
+
const { join } = await import("node:path");
|
|
398
|
+
|
|
399
|
+
const snapshotPath = join(getRevisionsDir(), revisionId, "snapshot.json");
|
|
400
|
+
|
|
401
|
+
writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), { mode: 0o600 });
|
|
402
|
+
chmodSync(snapshotPath, 0o600);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Cancel ongoing refresh
|
|
407
|
+
*/
|
|
408
|
+
cancel(): void {
|
|
409
|
+
if (this.abortController) {
|
|
410
|
+
this.abortController.abort();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get the secret provider instance
|
|
417
|
+
*/
|
|
418
|
+
export function getSecretProvider(): SecretProvider {
|
|
419
|
+
return SecretProvider.getInstance();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Await provider readiness
|
|
424
|
+
*/
|
|
425
|
+
export async function awaitProviderReady(timeoutMs: number = 30000): Promise<boolean> {
|
|
426
|
+
const provider = SecretProvider.getInstance();
|
|
427
|
+
const start = Date.now();
|
|
428
|
+
|
|
429
|
+
while (!provider.isReady() && provider.getStatus().status !== "unconfigured") {
|
|
430
|
+
if (Date.now() - start > timeoutMs) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return provider.isReady();
|
|
437
|
+
}
|