@haaaiawd/second-nature 0.1.24 → 0.1.26
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/index.js +78 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -5
- package/runtime/cli/commands/goal.d.ts +28 -0
- package/runtime/cli/commands/goal.js +163 -0
- package/runtime/cli/commands/index.js +38 -3
- package/runtime/cli/explain/resolve-subject.js +3 -0
- package/runtime/cli/ops/ops-router.d.ts +1 -1
- package/runtime/cli/ops/ops-router.js +63 -1
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
- package/runtime/cli/read-models/index.d.ts +14 -2
- package/runtime/cli/read-models/index.js +403 -101
- package/runtime/cli/read-models/types.d.ts +90 -3
- package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
- package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +11 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +78 -10
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
- package/runtime/core/second-nature/index.d.ts +1 -0
- package/runtime/core/second-nature/index.js +1 -0
- package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +16 -3
- package/runtime/core/second-nature/orchestrator/goal-priority.js +10 -9
- package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
- package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
- package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
- package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
- package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +3 -1
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +39 -1
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +21 -2
- package/runtime/guidance/draft-outreach-message.js +14 -1
- package/runtime/guidance/outreach-draft-schema.d.ts +104 -0
- package/runtime/guidance/outreach-draft-schema.js +14 -0
- package/runtime/observability/audit/audit-envelope.d.ts +1 -1
- package/runtime/observability/query/explain-query.d.ts +3 -0
- package/runtime/observability/query/explain-query.js +9 -0
- package/runtime/observability/services/lived-experience-audit.d.ts +22 -0
- package/runtime/observability/services/lived-experience-audit.js +30 -0
- package/runtime/shared/types/credential.d.ts +1 -1
- package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
- package/runtime/storage/db/schema/narrative-state.d.ts +1 -1
- package/runtime/storage/db/schema/narrative-state.js +2 -2
- package/runtime/storage/services/credential-vault.d.ts +18 -0
- package/runtime/storage/services/credential-vault.js +73 -3
|
@@ -97,6 +97,46 @@ export declare const sceneGuidanceRequestSchema: z.ZodObject<{
|
|
|
97
97
|
"zh-CN": "zh-CN";
|
|
98
98
|
}>>;
|
|
99
99
|
}, z.core.$strip>;
|
|
100
|
+
export declare const outreachNarrativeContextSchema: z.ZodObject<{
|
|
101
|
+
focus: z.ZodOptional<z.ZodString>;
|
|
102
|
+
progress: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
103
|
+
nextIntent: z.ZodOptional<z.ZodString>;
|
|
104
|
+
sourceRefs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
105
|
+
id: z.ZodString;
|
|
106
|
+
kind: z.ZodEnum<{
|
|
107
|
+
platform_item: "platform_item";
|
|
108
|
+
workspace_artifact: "workspace_artifact";
|
|
109
|
+
decision_record: "decision_record";
|
|
110
|
+
user_anchor: "user_anchor";
|
|
111
|
+
connector_result: "connector_result";
|
|
112
|
+
host_report: "host_report";
|
|
113
|
+
fallback_artifact: "fallback_artifact";
|
|
114
|
+
}>;
|
|
115
|
+
uri: z.ZodString;
|
|
116
|
+
excerptHash: z.ZodOptional<z.ZodString>;
|
|
117
|
+
observedAt: z.ZodOptional<z.ZodString>;
|
|
118
|
+
}, z.core.$strip>>>;
|
|
119
|
+
}, z.core.$strip>;
|
|
120
|
+
export declare const outreachRelationshipContextSchema: z.ZodObject<{
|
|
121
|
+
tone: z.ZodOptional<z.ZodString>;
|
|
122
|
+
topicAffinities: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
123
|
+
avgAffinity: z.ZodOptional<z.ZodNumber>;
|
|
124
|
+
sourceRefs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
125
|
+
id: z.ZodString;
|
|
126
|
+
kind: z.ZodEnum<{
|
|
127
|
+
platform_item: "platform_item";
|
|
128
|
+
workspace_artifact: "workspace_artifact";
|
|
129
|
+
decision_record: "decision_record";
|
|
130
|
+
user_anchor: "user_anchor";
|
|
131
|
+
connector_result: "connector_result";
|
|
132
|
+
host_report: "host_report";
|
|
133
|
+
fallback_artifact: "fallback_artifact";
|
|
134
|
+
}>;
|
|
135
|
+
uri: z.ZodString;
|
|
136
|
+
excerptHash: z.ZodOptional<z.ZodString>;
|
|
137
|
+
observedAt: z.ZodOptional<z.ZodString>;
|
|
138
|
+
}, z.core.$strip>>>;
|
|
139
|
+
}, z.core.$strip>;
|
|
100
140
|
export declare const outreachDraftRequestSchema: z.ZodObject<{
|
|
101
141
|
requestId: z.ZodString;
|
|
102
142
|
runtimeScope: z.ZodEnum<{
|
|
@@ -177,6 +217,46 @@ export declare const outreachDraftRequestSchema: z.ZodObject<{
|
|
|
177
217
|
excerptHash: z.ZodOptional<z.ZodString>;
|
|
178
218
|
observedAt: z.ZodOptional<z.ZodString>;
|
|
179
219
|
}, z.core.$strip>>;
|
|
220
|
+
narrativeContext: z.ZodOptional<z.ZodObject<{
|
|
221
|
+
focus: z.ZodOptional<z.ZodString>;
|
|
222
|
+
progress: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
223
|
+
nextIntent: z.ZodOptional<z.ZodString>;
|
|
224
|
+
sourceRefs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
225
|
+
id: z.ZodString;
|
|
226
|
+
kind: z.ZodEnum<{
|
|
227
|
+
platform_item: "platform_item";
|
|
228
|
+
workspace_artifact: "workspace_artifact";
|
|
229
|
+
decision_record: "decision_record";
|
|
230
|
+
user_anchor: "user_anchor";
|
|
231
|
+
connector_result: "connector_result";
|
|
232
|
+
host_report: "host_report";
|
|
233
|
+
fallback_artifact: "fallback_artifact";
|
|
234
|
+
}>;
|
|
235
|
+
uri: z.ZodString;
|
|
236
|
+
excerptHash: z.ZodOptional<z.ZodString>;
|
|
237
|
+
observedAt: z.ZodOptional<z.ZodString>;
|
|
238
|
+
}, z.core.$strip>>>;
|
|
239
|
+
}, z.core.$strip>>;
|
|
240
|
+
relationshipContext: z.ZodOptional<z.ZodObject<{
|
|
241
|
+
tone: z.ZodOptional<z.ZodString>;
|
|
242
|
+
topicAffinities: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
243
|
+
avgAffinity: z.ZodOptional<z.ZodNumber>;
|
|
244
|
+
sourceRefs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
245
|
+
id: z.ZodString;
|
|
246
|
+
kind: z.ZodEnum<{
|
|
247
|
+
platform_item: "platform_item";
|
|
248
|
+
workspace_artifact: "workspace_artifact";
|
|
249
|
+
decision_record: "decision_record";
|
|
250
|
+
user_anchor: "user_anchor";
|
|
251
|
+
connector_result: "connector_result";
|
|
252
|
+
host_report: "host_report";
|
|
253
|
+
fallback_artifact: "fallback_artifact";
|
|
254
|
+
}>;
|
|
255
|
+
uri: z.ZodString;
|
|
256
|
+
excerptHash: z.ZodOptional<z.ZodString>;
|
|
257
|
+
observedAt: z.ZodOptional<z.ZodString>;
|
|
258
|
+
}, z.core.$strip>>>;
|
|
259
|
+
}, z.core.$strip>>;
|
|
180
260
|
}, z.core.$strip>;
|
|
181
261
|
export type SceneGuidanceRequest = z.infer<typeof sceneGuidanceRequestSchema>;
|
|
182
262
|
export type OutreachDraftRequest = z.infer<typeof outreachDraftRequestSchema>;
|
|
@@ -212,6 +292,30 @@ export declare function safeParseOutreachDraftRequest(input: unknown): z.ZodSafe
|
|
|
212
292
|
fallbackRef?: string | undefined;
|
|
213
293
|
} | undefined;
|
|
214
294
|
language?: "en-US" | "zh-CN" | undefined;
|
|
295
|
+
narrativeContext?: {
|
|
296
|
+
focus?: string | undefined;
|
|
297
|
+
progress?: string[] | undefined;
|
|
298
|
+
nextIntent?: string | undefined;
|
|
299
|
+
sourceRefs?: {
|
|
300
|
+
id: string;
|
|
301
|
+
kind: "platform_item" | "workspace_artifact" | "decision_record" | "user_anchor" | "connector_result" | "host_report" | "fallback_artifact";
|
|
302
|
+
uri: string;
|
|
303
|
+
excerptHash?: string | undefined;
|
|
304
|
+
observedAt?: string | undefined;
|
|
305
|
+
}[] | undefined;
|
|
306
|
+
} | undefined;
|
|
307
|
+
relationshipContext?: {
|
|
308
|
+
tone?: string | undefined;
|
|
309
|
+
topicAffinities?: string[] | undefined;
|
|
310
|
+
avgAffinity?: number | undefined;
|
|
311
|
+
sourceRefs?: {
|
|
312
|
+
id: string;
|
|
313
|
+
kind: "platform_item" | "workspace_artifact" | "decision_record" | "user_anchor" | "connector_result" | "host_report" | "fallback_artifact";
|
|
314
|
+
uri: string;
|
|
315
|
+
excerptHash?: string | undefined;
|
|
316
|
+
observedAt?: string | undefined;
|
|
317
|
+
}[] | undefined;
|
|
318
|
+
} | undefined;
|
|
215
319
|
}>;
|
|
216
320
|
/** Async seam for generative outreach copy (implementation lives outside control-plane). */
|
|
217
321
|
export interface GuidanceDraftPort {
|
|
@@ -54,6 +54,18 @@ export const sceneGuidanceRequestSchema = z.object({
|
|
|
54
54
|
deliveryContext: deliveryExpressionContextSchema.optional(),
|
|
55
55
|
language: z.enum(["zh-CN", "en-US"]).optional(),
|
|
56
56
|
});
|
|
57
|
+
export const outreachNarrativeContextSchema = z.object({
|
|
58
|
+
focus: z.string().optional(),
|
|
59
|
+
progress: z.array(z.string()).optional(),
|
|
60
|
+
nextIntent: z.string().optional(),
|
|
61
|
+
sourceRefs: z.array(guidanceSourceRefSchema).optional(),
|
|
62
|
+
});
|
|
63
|
+
export const outreachRelationshipContextSchema = z.object({
|
|
64
|
+
tone: z.string().optional(),
|
|
65
|
+
topicAffinities: z.array(z.string()).optional(),
|
|
66
|
+
avgAffinity: z.number().optional(),
|
|
67
|
+
sourceRefs: z.array(guidanceSourceRefSchema).optional(),
|
|
68
|
+
});
|
|
57
69
|
export const outreachDraftRequestSchema = sceneGuidanceRequestSchema
|
|
58
70
|
.extend({
|
|
59
71
|
sceneType: z.enum(["outreach", "fallback_candidate"]),
|
|
@@ -62,6 +74,8 @@ export const outreachDraftRequestSchema = sceneGuidanceRequestSchema
|
|
|
62
74
|
judgmentVerdict: z.enum(["allow", "deny", "defer"]),
|
|
63
75
|
valueScore: z.number(),
|
|
64
76
|
interestRefs: z.array(guidanceSourceRefSchema),
|
|
77
|
+
narrativeContext: outreachNarrativeContextSchema.optional(),
|
|
78
|
+
relationshipContext: outreachRelationshipContextSchema.optional(),
|
|
65
79
|
})
|
|
66
80
|
.superRefine((val, ctx) => {
|
|
67
81
|
if (!val.deliveryContext) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RedactionManifest as FieldRedactionManifest } from "../redaction/manifest.js";
|
|
2
2
|
export type AuditPlane = "decision" | "delivery" | "source_coverage" | "governance" | "telemetry";
|
|
3
|
-
export type AuditEventFamily = "heartbeat.decision" | "delivery" | "source_coverage" | "guidance.grounding" | "host_capability" | "connector.attempt" | "state.governance";
|
|
3
|
+
export type AuditEventFamily = "heartbeat.decision" | "delivery" | "source_coverage" | "guidance.grounding" | "host_capability" | "connector.attempt" | "state.governance" | "narrative.trace" | "dream.trace";
|
|
4
4
|
export type AuditEnvelopeSensitivity = "public" | "internal" | "private" | "credential" | "sensitive";
|
|
5
5
|
export interface AuditRedactionManifest {
|
|
6
6
|
manifestId: string;
|
|
@@ -60,6 +60,15 @@ function eventMatchesQuery(envelope, query) {
|
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
case "relationship": {
|
|
64
|
+
const needle = query.relationshipId;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(payload).includes(needle);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
74
|
function summarizeEnvelope(e) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppendOnlyAuditStore } from "../audit/append-only-audit-store.js";
|
|
2
2
|
import type { SourceRef } from "../../storage/life-evidence/types.js";
|
|
3
|
+
import type { DreamTrace } from "../../dream/types.js";
|
|
3
4
|
export type RuntimeScope = "rhythm" | "user_task" | "user_reply";
|
|
4
5
|
export type HeartbeatOutcome = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
|
|
5
6
|
export type DeliveryAuditStatus = "not_requested" | "target_available" | "target_none" | "channel_missing" | "host_unsupported" | "ack_dropped" | "sent" | "failed" | "not_sent_fallback";
|
|
@@ -66,6 +67,21 @@ export interface GuidanceGroundingAuditPayload {
|
|
|
66
67
|
deliveryWording?: "sendable" | "not_sent_fallback_candidate";
|
|
67
68
|
createdAt: string;
|
|
68
69
|
}
|
|
70
|
+
export interface NarrativeTracePayload {
|
|
71
|
+
traceId: string;
|
|
72
|
+
narrativeId: string;
|
|
73
|
+
revision: number;
|
|
74
|
+
updateSource: "heartbeat" | "dream" | "owner" | "maintenance";
|
|
75
|
+
sourceRefs: Array<{
|
|
76
|
+
id: string;
|
|
77
|
+
kind: string;
|
|
78
|
+
uri?: string;
|
|
79
|
+
}>;
|
|
80
|
+
unsupportedClaims: string[];
|
|
81
|
+
groundingStatus: GroundingStatus;
|
|
82
|
+
goalInfluenceRefs: string[];
|
|
83
|
+
createdAt: string;
|
|
84
|
+
}
|
|
69
85
|
export interface ExplainLinkageSummary {
|
|
70
86
|
decisionId: string;
|
|
71
87
|
summary: string;
|
|
@@ -92,6 +108,12 @@ export declare class LivedExperienceAuditRecorder {
|
|
|
92
108
|
recordGuidanceGrounding(payload: GuidanceGroundingAuditPayload): {
|
|
93
109
|
eventId: string;
|
|
94
110
|
};
|
|
111
|
+
recordNarrativeTrace(payload: NarrativeTracePayload): {
|
|
112
|
+
eventId: string;
|
|
113
|
+
};
|
|
114
|
+
recordDreamTrace(payload: DreamTrace): {
|
|
115
|
+
eventId: string;
|
|
116
|
+
};
|
|
95
117
|
explainLinkageForDecision(decisionId: string): ExplainLinkageSummary;
|
|
96
118
|
}
|
|
97
119
|
export declare function createLivedExperienceAuditRecorder(store?: AppendOnlyAuditStore): LivedExperienceAuditRecorder;
|
|
@@ -133,6 +133,36 @@ export class LivedExperienceAuditRecorder {
|
|
|
133
133
|
}
|
|
134
134
|
return { eventId: envelope.eventId };
|
|
135
135
|
}
|
|
136
|
+
recordNarrativeTrace(payload) {
|
|
137
|
+
const seq = this.bumpSequence();
|
|
138
|
+
const envelope = buildAuditEnvelope({
|
|
139
|
+
family: "narrative.trace",
|
|
140
|
+
plane: "source_coverage",
|
|
141
|
+
traceId: payload.traceId,
|
|
142
|
+
sequence: seq,
|
|
143
|
+
payload,
|
|
144
|
+
previousHash: this.store.lastRecordHash(),
|
|
145
|
+
eventId: crypto.randomUUID(),
|
|
146
|
+
createdAt: payload.createdAt,
|
|
147
|
+
});
|
|
148
|
+
this.store.append(envelope);
|
|
149
|
+
return { eventId: envelope.eventId };
|
|
150
|
+
}
|
|
151
|
+
recordDreamTrace(payload) {
|
|
152
|
+
const seq = this.bumpSequence();
|
|
153
|
+
const envelope = buildAuditEnvelope({
|
|
154
|
+
family: "dream.trace",
|
|
155
|
+
plane: "telemetry",
|
|
156
|
+
traceId: payload.traceId,
|
|
157
|
+
sequence: seq,
|
|
158
|
+
payload,
|
|
159
|
+
previousHash: this.store.lastRecordHash(),
|
|
160
|
+
eventId: crypto.randomUUID(),
|
|
161
|
+
createdAt: payload.finishedAt,
|
|
162
|
+
});
|
|
163
|
+
this.store.append(envelope);
|
|
164
|
+
return { eventId: envelope.eventId };
|
|
165
|
+
}
|
|
136
166
|
explainLinkageForDecision(decisionId) {
|
|
137
167
|
const entry = this.explainIndex.get(decisionId);
|
|
138
168
|
const warnings = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CredentialState = "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed";
|
|
1
|
+
export type CredentialState = "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed" | "decrypt_failed";
|
|
2
2
|
export type CredentialType = "api_key" | "oauth_token" | "node_secret" | "verification_code";
|
|
3
3
|
export interface CredentialContext {
|
|
4
4
|
platformId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StateDatabase } from "../db/index.js";
|
|
2
|
-
export type ChronicleEventKind = "heartbeat" | "connector_action" | "outreach" | "owner_reply" | "dream_run" | "maintenance";
|
|
2
|
+
export type ChronicleEventKind = "heartbeat" | "connector_action" | "outreach" | "owner_reply" | "dream_run" | "maintenance" | "system_notice";
|
|
3
3
|
export interface SourceRef {
|
|
4
4
|
sourceId: string;
|
|
5
5
|
kind: string;
|
|
@@ -99,7 +99,7 @@ export declare const narrativeState: import("drizzle-orm/sqlite-core").SQLiteTab
|
|
|
99
99
|
name: "confidence";
|
|
100
100
|
tableName: "narrative_state";
|
|
101
101
|
dataType: "number";
|
|
102
|
-
columnType: "
|
|
102
|
+
columnType: "SQLiteReal";
|
|
103
103
|
data: number;
|
|
104
104
|
driverParam: number;
|
|
105
105
|
notNull: true;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { index, sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { index, sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
|
|
2
2
|
export const narrativeState = sqliteTable("narrative_state", {
|
|
3
3
|
narrativeId: text("narrative_id").primaryKey(),
|
|
4
4
|
revision: integer("revision").notNull().default(1),
|
|
5
5
|
focus: text("focus").notNull(),
|
|
6
6
|
progressJson: text("progress_json").notNull(),
|
|
7
7
|
nextIntent: text("next_intent").notNull(),
|
|
8
|
-
confidence:
|
|
8
|
+
confidence: real("confidence").notNull().default(0),
|
|
9
9
|
sourceRefsJson: text("source_refs_json").notNull(),
|
|
10
10
|
unsupportedClaimsJson: text("unsupported_claims_json").notNull(),
|
|
11
11
|
status: text("status").notNull(),
|
|
@@ -10,4 +10,22 @@ export interface CredentialVault {
|
|
|
10
10
|
loadCredentialContext(platformId: string): Promise<CredentialContext | null>;
|
|
11
11
|
getCredentialState(platformId: string): Promise<CredentialState>;
|
|
12
12
|
}
|
|
13
|
+
/** T1.4.1 — runtime secret health probe result for a single credential row. */
|
|
14
|
+
export interface CredentialHealthProbe {
|
|
15
|
+
platformId: string;
|
|
16
|
+
state: CredentialState | "decrypt_failed";
|
|
17
|
+
keyHealth: "missing_key" | "wrong_key" | "ok";
|
|
18
|
+
hasBaseUrl: boolean;
|
|
19
|
+
diagnosticCode: "missing_runtime_secret" | "credential_recovery_required" | "ok";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* T1.4.1 — probe a credential record for runtime secret health.
|
|
23
|
+
*
|
|
24
|
+
* Given a raw encrypted value from the DB, this function checks:
|
|
25
|
+
* 1. Is SECOND_NATURE_ENCRYPTION_KEY present and >= 32 chars?
|
|
26
|
+
* 2. Can the ciphertext be decrypted with that key?
|
|
27
|
+
*
|
|
28
|
+
* It never throws; all failures are encoded in the returned state.
|
|
29
|
+
*/
|
|
30
|
+
export declare function probeCredentialHealth(platformId: string, encryptedValue: string | undefined | null, baseUrl: string | undefined | null): CredentialHealthProbe;
|
|
13
31
|
export declare function createCredentialVault(db: StateDatabase["db"]): CredentialVault;
|
|
@@ -54,6 +54,58 @@ export function decryptCredentialAtRest(ciphertext) {
|
|
|
54
54
|
return "";
|
|
55
55
|
return decryptInternal(ciphertext);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* T1.4.1 — probe a credential record for runtime secret health.
|
|
59
|
+
*
|
|
60
|
+
* Given a raw encrypted value from the DB, this function checks:
|
|
61
|
+
* 1. Is SECOND_NATURE_ENCRYPTION_KEY present and >= 32 chars?
|
|
62
|
+
* 2. Can the ciphertext be decrypted with that key?
|
|
63
|
+
*
|
|
64
|
+
* It never throws; all failures are encoded in the returned state.
|
|
65
|
+
*/
|
|
66
|
+
export function probeCredentialHealth(platformId, encryptedValue, baseUrl) {
|
|
67
|
+
// Key availability check
|
|
68
|
+
const rawKey = process.env.SECOND_NATURE_ENCRYPTION_KEY?.trim();
|
|
69
|
+
if (!rawKey || rawKey.length < 32) {
|
|
70
|
+
return {
|
|
71
|
+
platformId,
|
|
72
|
+
state: encryptedValue ? "decrypt_failed" : "missing",
|
|
73
|
+
keyHealth: "missing_key",
|
|
74
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
75
|
+
diagnosticCode: "missing_runtime_secret",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// No encrypted value to test
|
|
79
|
+
if (!encryptedValue) {
|
|
80
|
+
return {
|
|
81
|
+
platformId,
|
|
82
|
+
state: "missing",
|
|
83
|
+
keyHealth: "ok",
|
|
84
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
85
|
+
diagnosticCode: "ok",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Decryption attempt
|
|
89
|
+
try {
|
|
90
|
+
decryptCredentialAtRest(encryptedValue);
|
|
91
|
+
return {
|
|
92
|
+
platformId,
|
|
93
|
+
state: "active",
|
|
94
|
+
keyHealth: "ok",
|
|
95
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
96
|
+
diagnosticCode: "ok",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return {
|
|
101
|
+
platformId,
|
|
102
|
+
state: "decrypt_failed",
|
|
103
|
+
keyHealth: "wrong_key",
|
|
104
|
+
hasBaseUrl: Boolean(baseUrl),
|
|
105
|
+
diagnosticCode: "credential_recovery_required",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
57
109
|
export function createCredentialVault(db) {
|
|
58
110
|
return {
|
|
59
111
|
async saveCredentialContext(input) {
|
|
@@ -89,16 +141,34 @@ export function createCredentialVault(db) {
|
|
|
89
141
|
if (!record)
|
|
90
142
|
return null;
|
|
91
143
|
let plain;
|
|
144
|
+
let status = record.status;
|
|
92
145
|
if (record.encryptedValue) {
|
|
93
146
|
if (!isCredentialCiphertext(record.encryptedValue)) {
|
|
94
|
-
|
|
147
|
+
// Fail-closed: return decrypt_failed so callers do not crash.
|
|
148
|
+
return {
|
|
149
|
+
platformId: record.platformId,
|
|
150
|
+
credentialType: record.credentialType,
|
|
151
|
+
status: "decrypt_failed",
|
|
152
|
+
encryptedValue: undefined,
|
|
153
|
+
verificationCode: record.verificationCode ?? undefined,
|
|
154
|
+
challengeText: record.challengeText ?? undefined,
|
|
155
|
+
verificationDeadline: record.expiresAt ?? undefined,
|
|
156
|
+
attemptsRemaining: record.attemptsRemaining ?? undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
plain = decryptCredentialAtRest(record.encryptedValue);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Decryption failure must not break the whole state load.
|
|
164
|
+
status = "decrypt_failed";
|
|
165
|
+
plain = undefined;
|
|
95
166
|
}
|
|
96
|
-
plain = decryptCredentialAtRest(record.encryptedValue);
|
|
97
167
|
}
|
|
98
168
|
return {
|
|
99
169
|
platformId: record.platformId,
|
|
100
170
|
credentialType: record.credentialType,
|
|
101
|
-
status
|
|
171
|
+
status,
|
|
102
172
|
encryptedValue: plain,
|
|
103
173
|
verificationCode: record.verificationCode ?? undefined,
|
|
104
174
|
challengeText: record.challengeText ?? undefined,
|