@haaaiawd/second-nature 0.1.18 → 0.1.19
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 +855 -855
- package/openclaw.plugin.json +29 -29
- package/package.json +52 -52
- package/runtime/cli/commands/index.d.ts +14 -14
- package/runtime/cli/commands/index.js +193 -193
- package/runtime/cli/explain/explain-surface-subject.d.ts +8 -8
- package/runtime/cli/explain/explain-surface-subject.js +9 -9
- package/runtime/cli/explain/format-explanation.d.ts +12 -12
- package/runtime/cli/explain/format-explanation.js +12 -12
- package/runtime/cli/explain/resolve-subject.js +41 -41
- package/runtime/cli/host-capability/classify-delivery.d.ts +14 -14
- package/runtime/cli/host-capability/classify-delivery.js +20 -20
- package/runtime/cli/host-capability/probe-host-capability.d.ts +2 -2
- package/runtime/cli/host-capability/probe-host-capability.js +58 -58
- package/runtime/cli/host-capability/record-host-capability.d.ts +6 -6
- package/runtime/cli/host-capability/record-host-capability.js +14 -14
- package/runtime/cli/host-capability/types.d.ts +71 -71
- package/runtime/cli/host-capability/types.js +6 -6
- package/runtime/cli/host-smoke/run-host-smoke.d.ts +2 -2
- package/runtime/cli/host-smoke/run-host-smoke.js +40 -40
- package/runtime/cli/host-smoke/types.d.ts +35 -35
- package/runtime/cli/host-smoke/types.js +6 -6
- package/runtime/cli/index.js +65 -58
- package/runtime/cli/ops/heartbeat-surface.d.ts +45 -38
- package/runtime/cli/ops/heartbeat-surface.js +79 -73
- package/runtime/cli/ops/ops-router.d.ts +26 -19
- package/runtime/cli/ops/ops-router.js +102 -89
- package/runtime/cli/ops/show-operator-fallback.d.ts +13 -13
- package/runtime/cli/ops/show-operator-fallback.js +22 -22
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +40 -19
- package/runtime/cli/ops/workspace-heartbeat-runner.js +93 -39
- package/runtime/cli/read-models/index.d.ts +35 -29
- package/runtime/cli/read-models/index.js +365 -256
- package/runtime/cli/read-models/operator-explain-map.d.ts +6 -6
- package/runtime/cli/read-models/operator-explain-map.js +10 -10
- package/runtime/cli/read-models/types.d.ts +112 -79
- package/runtime/cli/runtime/runtime-artifact-boundary.d.ts +28 -28
- package/runtime/cli/runtime/runtime-artifact-boundary.js +94 -94
- package/runtime/connectors/base/contract.d.ts +87 -87
- package/runtime/connectors/base/execution-policy.d.ts +47 -47
- package/runtime/connectors/base/execution-policy.js +82 -82
- package/runtime/connectors/base/index.d.ts +8 -8
- package/runtime/connectors/base/index.js +8 -8
- package/runtime/connectors/base/manifest.d.ts +64 -64
- package/runtime/connectors/base/manifest.js +86 -86
- package/runtime/connectors/base/map-life-evidence.d.ts +16 -16
- package/runtime/connectors/base/map-life-evidence.js +79 -79
- package/runtime/connectors/base/policy-layer.d.ts +29 -29
- package/runtime/connectors/base/policy-layer.js +198 -198
- package/runtime/connectors/base/route-planner.js +99 -99
- package/runtime/connectors/index.d.ts +5 -5
- package/runtime/connectors/index.js +5 -5
- package/runtime/connectors/near-real/near-real-connector-smoke.d.ts +19 -19
- package/runtime/connectors/near-real/near-real-connector-smoke.js +152 -152
- package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +114 -114
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +63 -63
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +162 -139
- package/runtime/core/second-nature/heartbeat/index.d.ts +8 -8
- package/runtime/core/second-nature/heartbeat/index.js +7 -7
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.d.ts +21 -21
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.js +35 -35
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +28 -28
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +35 -35
- package/runtime/core/second-nature/heartbeat/signal.d.ts +42 -42
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +51 -51
- package/runtime/core/second-nature/index.d.ts +22 -22
- package/runtime/core/second-nature/index.js +22 -22
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +100 -100
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +144 -144
- package/runtime/core/second-nature/orchestrator/guard-layer.d.ts +8 -8
- package/runtime/core/second-nature/orchestrator/guard-layer.js +110 -110
- package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +13 -13
- package/runtime/core/second-nature/orchestrator/intent-planner.js +199 -199
- package/runtime/core/second-nature/orchestrator/lease-manager.d.ts +14 -14
- package/runtime/core/second-nature/orchestrator/lease-manager.js +58 -58
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +6 -6
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +63 -63
- package/runtime/core/second-nature/outreach/delivery-target.d.ts +26 -26
- package/runtime/core/second-nature/outreach/delivery-target.js +70 -70
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +38 -38
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +119 -119
- package/runtime/core/second-nature/outreach/judge-input-from-snapshot.d.ts +7 -7
- package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +45 -45
- package/runtime/core/second-nature/outreach/judge-outreach.d.ts +40 -40
- package/runtime/core/second-nature/outreach/judge-outreach.js +121 -121
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +21 -21
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +123 -123
- package/runtime/core/second-nature/rhythm/planner-rhythm-window.d.ts +15 -15
- package/runtime/core/second-nature/rhythm/planner-rhythm-window.js +52 -52
- package/runtime/core/second-nature/rhythm/policy-bridge.d.ts +19 -19
- package/runtime/core/second-nature/rhythm/policy-bridge.js +34 -34
- package/runtime/core/second-nature/runtime/service-entry.js +45 -45
- package/runtime/core/second-nature/types.d.ts +51 -51
- package/runtime/guidance/draft-outreach-message.d.ts +7 -7
- package/runtime/guidance/draft-outreach-message.js +42 -42
- package/runtime/guidance/evidence-guidance.d.ts +40 -40
- package/runtime/guidance/evidence-guidance.js +52 -52
- package/runtime/guidance/index.d.ts +11 -11
- package/runtime/guidance/index.js +11 -11
- package/runtime/guidance/outreach-draft-schema.d.ts +228 -228
- package/runtime/guidance/outreach-draft-schema.js +80 -80
- package/runtime/observability/audit/append-only-audit-store.d.ts +14 -14
- package/runtime/observability/audit/append-only-audit-store.js +21 -21
- package/runtime/observability/audit/audit-envelope.d.ts +51 -51
- package/runtime/observability/audit/audit-envelope.js +130 -130
- package/runtime/observability/audit/verify-audit-hash-chain.d.ts +23 -23
- package/runtime/observability/audit/verify-audit-hash-chain.js +83 -83
- package/runtime/observability/db/index.js +47 -47
- package/runtime/observability/db/schema/host-capability-reports.d.ts +180 -180
- package/runtime/observability/db/schema/host-capability-reports.js +12 -12
- package/runtime/observability/db/schema/index.d.ts +947 -947
- package/runtime/observability/db/schema/index.js +71 -71
- package/runtime/observability/index.d.ts +20 -20
- package/runtime/observability/index.js +19 -19
- package/runtime/observability/query/explain-query.d.ts +48 -48
- package/runtime/observability/query/explain-query.js +114 -114
- package/runtime/observability/query/export-audit-bundle.d.ts +22 -22
- package/runtime/observability/query/export-audit-bundle.js +27 -27
- package/runtime/observability/services/decision-ledger.d.ts +46 -46
- package/runtime/observability/services/decision-ledger.js +161 -161
- package/runtime/observability/services/governance-audit.d.ts +41 -41
- package/runtime/observability/services/governance-audit.js +163 -163
- package/runtime/observability/services/governance-plane-recorder.d.ts +47 -47
- package/runtime/observability/services/governance-plane-recorder.js +55 -55
- package/runtime/observability/services/lived-experience-audit.d.ts +97 -97
- package/runtime/observability/services/lived-experience-audit.js +162 -162
- package/runtime/observability/services/runtime-decision-recorder.d.ts +29 -29
- package/runtime/observability/services/runtime-decision-recorder.js +94 -94
- package/runtime/storage/bootstrap/native-sqlite-probe.d.ts +7 -7
- package/runtime/storage/bootstrap/native-sqlite-probe.js +28 -28
- package/runtime/storage/bootstrap/repair-gate.d.ts +17 -17
- package/runtime/storage/bootstrap/repair-gate.js +71 -71
- package/runtime/storage/bootstrap/storage-mode-smoke.d.ts +38 -38
- package/runtime/storage/bootstrap/storage-mode-smoke.js +85 -85
- package/runtime/storage/db/index.js +61 -61
- package/runtime/storage/db/schema/delivery-attempts.d.ts +199 -199
- package/runtime/storage/db/schema/delivery-attempts.js +13 -13
- package/runtime/storage/db/schema/index.d.ts +9 -9
- package/runtime/storage/db/schema/index.js +9 -9
- package/runtime/storage/db/schema/life-evidence-index.d.ts +161 -161
- package/runtime/storage/db/schema/life-evidence-index.js +11 -11
- package/runtime/storage/db/schema/operator-fallback-artifacts.d.ts +161 -161
- package/runtime/storage/db/schema/operator-fallback-artifacts.js +11 -11
- package/runtime/storage/db/schema/policies.d.ts +98 -98
- package/runtime/storage/db/schema/policies.js +8 -8
- package/runtime/storage/delivery/query-delivery-attempts.d.ts +3 -3
- package/runtime/storage/delivery/query-delivery-attempts.js +32 -32
- package/runtime/storage/delivery/types.d.ts +27 -27
- package/runtime/storage/delivery/types.js +1 -1
- package/runtime/storage/delivery/write-delivery-attempt.d.ts +6 -6
- package/runtime/storage/delivery/write-delivery-attempt.js +36 -36
- package/runtime/storage/fallback/load-operator-fallback.d.ts +14 -14
- package/runtime/storage/fallback/load-operator-fallback.js +47 -47
- package/runtime/storage/fallback/operator-fallback-types.d.ts +9 -9
- package/runtime/storage/fallback/operator-fallback-types.js +1 -1
- package/runtime/storage/fallback/operator-fallback-view.d.ts +11 -11
- package/runtime/storage/fallback/operator-fallback-view.js +1 -1
- package/runtime/storage/fallback/write-operator-fallback.d.ts +6 -6
- package/runtime/storage/fallback/write-operator-fallback.js +21 -21
- package/runtime/storage/index.d.ts +37 -37
- package/runtime/storage/index.js +30 -30
- package/runtime/storage/life-evidence/append-life-evidence.d.ts +7 -7
- package/runtime/storage/life-evidence/append-life-evidence.js +64 -64
- package/runtime/storage/life-evidence/types.d.ts +45 -45
- package/runtime/storage/life-evidence/types.js +6 -6
- package/runtime/storage/quiet/persist-quiet-artifact.d.ts +7 -7
- package/runtime/storage/quiet/persist-quiet-artifact.js +22 -22
- package/runtime/storage/quiet/quiet-artifact-types.d.ts +18 -18
- package/runtime/storage/quiet/quiet-artifact-types.js +1 -1
- package/runtime/storage/quiet/quiet-artifact-writer.d.ts +15 -15
- package/runtime/storage/quiet/quiet-artifact-writer.js +56 -56
- package/runtime/storage/repositories/credential-repository.js +30 -30
- package/runtime/storage/rhythm/rhythm-policy-snapshot.d.ts +10 -10
- package/runtime/storage/rhythm/rhythm-policy-snapshot.js +34 -34
- package/runtime/storage/services/credential-vault.d.ts +13 -13
- package/runtime/storage/services/credential-vault.js +116 -116
- package/runtime/storage/snapshots/continuity-snapshot.d.ts +9 -9
- package/runtime/storage/snapshots/continuity-snapshot.js +41 -41
- package/runtime/storage/snapshots/life-evidence-snapshot.d.ts +6 -6
- package/runtime/storage/snapshots/life-evidence-snapshot.js +114 -114
- package/runtime/storage/snapshots/types.d.ts +58 -58
- package/runtime/storage/snapshots/types.js +1 -1
- package/runtime/storage/state-api.js +104 -104
- package/runtime/storage/user-interest/load-user-interest-snapshot.d.ts +2 -2
- package/runtime/storage/user-interest/load-user-interest-snapshot.js +150 -150
- package/runtime/storage/user-interest/types.d.ts +25 -25
- package/runtime/storage/user-interest/types.js +1 -1
- package/workspace-ops-bridge.js +81 -81
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
export class AppendOnlyAuditStore {
|
|
2
|
-
events = [];
|
|
3
|
-
append(envelope) {
|
|
4
|
-
const last = this.events[this.events.length - 1];
|
|
5
|
-
if (last) {
|
|
6
|
-
if (envelope.integrity.previousHash !== last.integrity.recordHash) {
|
|
7
|
-
throw new Error("audit_previous_hash_mismatch");
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
else if (envelope.integrity.previousHash !== undefined) {
|
|
11
|
-
throw new Error("audit_genesis_previous_hash");
|
|
12
|
-
}
|
|
13
|
-
this.events.push(envelope);
|
|
14
|
-
}
|
|
15
|
-
list() {
|
|
16
|
-
return this.events;
|
|
17
|
-
}
|
|
18
|
-
lastRecordHash() {
|
|
19
|
-
return this.events[this.events.length - 1]?.integrity.recordHash;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
1
|
+
export class AppendOnlyAuditStore {
|
|
2
|
+
events = [];
|
|
3
|
+
append(envelope) {
|
|
4
|
+
const last = this.events[this.events.length - 1];
|
|
5
|
+
if (last) {
|
|
6
|
+
if (envelope.integrity.previousHash !== last.integrity.recordHash) {
|
|
7
|
+
throw new Error("audit_previous_hash_mismatch");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
else if (envelope.integrity.previousHash !== undefined) {
|
|
11
|
+
throw new Error("audit_genesis_previous_hash");
|
|
12
|
+
}
|
|
13
|
+
this.events.push(envelope);
|
|
14
|
+
}
|
|
15
|
+
list() {
|
|
16
|
+
return this.events;
|
|
17
|
+
}
|
|
18
|
+
lastRecordHash() {
|
|
19
|
+
return this.events[this.events.length - 1]?.integrity.recordHash;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import { type RedactionManifest as FieldRedactionManifest } from "../redaction/manifest.js";
|
|
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";
|
|
4
|
-
export type AuditEnvelopeSensitivity = "public" | "internal" | "private" | "credential" | "sensitive";
|
|
5
|
-
export interface AuditRedactionManifest {
|
|
6
|
-
manifestId: string;
|
|
7
|
-
maskedPaths: string[];
|
|
8
|
-
erasedPaths: string[];
|
|
9
|
-
hashedPaths: string[];
|
|
10
|
-
contentRefPaths: string[];
|
|
11
|
-
sensitivity: AuditEnvelopeSensitivity;
|
|
12
|
-
}
|
|
13
|
-
export interface AuditIntegrity {
|
|
14
|
-
previousHash?: string;
|
|
15
|
-
recordHash: string;
|
|
16
|
-
schemaVersion: "observability.v5";
|
|
17
|
-
}
|
|
18
|
-
export interface AuditEnvelope<TPayload> {
|
|
19
|
-
eventId: string;
|
|
20
|
-
family: AuditEventFamily;
|
|
21
|
-
plane: AuditPlane;
|
|
22
|
-
traceId: string;
|
|
23
|
-
sequence: number;
|
|
24
|
-
createdAt: string;
|
|
25
|
-
payload: TPayload;
|
|
26
|
-
redaction: AuditRedactionManifest;
|
|
27
|
-
integrity: AuditIntegrity;
|
|
28
|
-
}
|
|
29
|
-
export interface RedactAuditEventResult<TPayload> {
|
|
30
|
-
payload: TPayload;
|
|
31
|
-
redaction: AuditRedactionManifest;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Apply field redaction rules and lift manifests to audit path vocabulary (observability-system.detail §2).
|
|
35
|
-
*/
|
|
36
|
-
export declare function redactAuditEvent<TPayload extends object>(payload: TPayload): RedactAuditEventResult<TPayload>;
|
|
37
|
-
export interface BuildAuditEnvelopeInput<TPayload extends object> {
|
|
38
|
-
family: AuditEventFamily;
|
|
39
|
-
plane: AuditPlane;
|
|
40
|
-
traceId: string;
|
|
41
|
-
sequence: number;
|
|
42
|
-
payload: TPayload;
|
|
43
|
-
previousHash?: string;
|
|
44
|
-
eventId?: string;
|
|
45
|
-
createdAt?: string;
|
|
46
|
-
}
|
|
47
|
-
/** Recompute integrity hash for an existing envelope (T5.2.2 / verifyAuditHashChain). */
|
|
48
|
-
export declare function computeAuditRecordHash(envelope: AuditEnvelope<unknown>): string;
|
|
49
|
-
export declare function buildAuditEnvelope<TPayload extends object>(input: BuildAuditEnvelopeInput<TPayload>): AuditEnvelope<TPayload>;
|
|
50
|
-
/** @internal Maps legacy field manifest to audit manifest for persistence helpers. */
|
|
51
|
-
export declare function auditManifestFromFieldManifest(manifest: FieldRedactionManifest): AuditRedactionManifest;
|
|
1
|
+
import { type RedactionManifest as FieldRedactionManifest } from "../redaction/manifest.js";
|
|
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";
|
|
4
|
+
export type AuditEnvelopeSensitivity = "public" | "internal" | "private" | "credential" | "sensitive";
|
|
5
|
+
export interface AuditRedactionManifest {
|
|
6
|
+
manifestId: string;
|
|
7
|
+
maskedPaths: string[];
|
|
8
|
+
erasedPaths: string[];
|
|
9
|
+
hashedPaths: string[];
|
|
10
|
+
contentRefPaths: string[];
|
|
11
|
+
sensitivity: AuditEnvelopeSensitivity;
|
|
12
|
+
}
|
|
13
|
+
export interface AuditIntegrity {
|
|
14
|
+
previousHash?: string;
|
|
15
|
+
recordHash: string;
|
|
16
|
+
schemaVersion: "observability.v5";
|
|
17
|
+
}
|
|
18
|
+
export interface AuditEnvelope<TPayload> {
|
|
19
|
+
eventId: string;
|
|
20
|
+
family: AuditEventFamily;
|
|
21
|
+
plane: AuditPlane;
|
|
22
|
+
traceId: string;
|
|
23
|
+
sequence: number;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
payload: TPayload;
|
|
26
|
+
redaction: AuditRedactionManifest;
|
|
27
|
+
integrity: AuditIntegrity;
|
|
28
|
+
}
|
|
29
|
+
export interface RedactAuditEventResult<TPayload> {
|
|
30
|
+
payload: TPayload;
|
|
31
|
+
redaction: AuditRedactionManifest;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Apply field redaction rules and lift manifests to audit path vocabulary (observability-system.detail §2).
|
|
35
|
+
*/
|
|
36
|
+
export declare function redactAuditEvent<TPayload extends object>(payload: TPayload): RedactAuditEventResult<TPayload>;
|
|
37
|
+
export interface BuildAuditEnvelopeInput<TPayload extends object> {
|
|
38
|
+
family: AuditEventFamily;
|
|
39
|
+
plane: AuditPlane;
|
|
40
|
+
traceId: string;
|
|
41
|
+
sequence: number;
|
|
42
|
+
payload: TPayload;
|
|
43
|
+
previousHash?: string;
|
|
44
|
+
eventId?: string;
|
|
45
|
+
createdAt?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Recompute integrity hash for an existing envelope (T5.2.2 / verifyAuditHashChain). */
|
|
48
|
+
export declare function computeAuditRecordHash(envelope: AuditEnvelope<unknown>): string;
|
|
49
|
+
export declare function buildAuditEnvelope<TPayload extends object>(input: BuildAuditEnvelopeInput<TPayload>): AuditEnvelope<TPayload>;
|
|
50
|
+
/** @internal Maps legacy field manifest to audit manifest for persistence helpers. */
|
|
51
|
+
export declare function auditManifestFromFieldManifest(manifest: FieldRedactionManifest): AuditRedactionManifest;
|
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Audit envelope construction + redaction for observability-system v5.
|
|
3
|
-
*
|
|
4
|
-
* Core logic: redact structured payloads, attach RedactionManifest (L0/L1 contract paths),
|
|
5
|
-
* compute hash-chain fields for append-only ledger rows.
|
|
6
|
-
*
|
|
7
|
-
* Dependencies: observability redactEvent (field-level); maps to envelope redaction paths.
|
|
8
|
-
*
|
|
9
|
-
* Boundaries: persistence is delegated to AppendOnlyAuditStore / DB adapters.
|
|
10
|
-
*
|
|
11
|
-
* Test coverage: tests/unit/observability/audit-envelope.test.ts
|
|
12
|
-
*/
|
|
13
|
-
import * as crypto from "node:crypto";
|
|
14
|
-
import { redactEvent } from "../redaction/manifest.js";
|
|
15
|
-
function fieldPathToAuditPath(field) {
|
|
16
|
-
if (field.startsWith("/")) {
|
|
17
|
-
return field;
|
|
18
|
-
}
|
|
19
|
-
return `/payload/${field.replace(/\./g, "/")}`;
|
|
20
|
-
}
|
|
21
|
-
function mapSensitivity(level) {
|
|
22
|
-
switch (level) {
|
|
23
|
-
case "public":
|
|
24
|
-
return "public";
|
|
25
|
-
case "internal":
|
|
26
|
-
return "internal";
|
|
27
|
-
case "confidential":
|
|
28
|
-
return "private";
|
|
29
|
-
case "restricted":
|
|
30
|
-
return "sensitive";
|
|
31
|
-
default:
|
|
32
|
-
return "internal";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Apply field redaction rules and lift manifests to audit path vocabulary (observability-system.detail §2).
|
|
37
|
-
*/
|
|
38
|
-
export function redactAuditEvent(payload) {
|
|
39
|
-
const { redacted, manifest } = redactEvent(payload);
|
|
40
|
-
const redaction = {
|
|
41
|
-
manifestId: manifest.id,
|
|
42
|
-
maskedPaths: manifest.maskedFields.map(fieldPathToAuditPath),
|
|
43
|
-
erasedPaths: manifest.erasedFields.map(fieldPathToAuditPath),
|
|
44
|
-
hashedPaths: manifest.hashedFields.map(fieldPathToAuditPath),
|
|
45
|
-
contentRefPaths: [],
|
|
46
|
-
sensitivity: mapSensitivity(manifest.sensitivityLevel),
|
|
47
|
-
};
|
|
48
|
-
return { payload: redacted, redaction };
|
|
49
|
-
}
|
|
50
|
-
function stableStringify(value) {
|
|
51
|
-
if (value === null || typeof value !== "object") {
|
|
52
|
-
return JSON.stringify(value);
|
|
53
|
-
}
|
|
54
|
-
if (Array.isArray(value)) {
|
|
55
|
-
return `[${value.map((v) => stableStringify(v)).join(",")}]`;
|
|
56
|
-
}
|
|
57
|
-
const obj = value;
|
|
58
|
-
const keys = Object.keys(obj).sort();
|
|
59
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
60
|
-
}
|
|
61
|
-
function hashRecord(input) {
|
|
62
|
-
const canonical = stableStringify({
|
|
63
|
-
eventId: input.eventId,
|
|
64
|
-
family: input.family,
|
|
65
|
-
plane: input.plane,
|
|
66
|
-
traceId: input.traceId,
|
|
67
|
-
sequence: input.sequence,
|
|
68
|
-
createdAt: input.createdAt,
|
|
69
|
-
payload: input.payload,
|
|
70
|
-
redaction: input.redaction,
|
|
71
|
-
previousHash: input.previousHash ?? null,
|
|
72
|
-
});
|
|
73
|
-
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
74
|
-
}
|
|
75
|
-
/** Recompute integrity hash for an existing envelope (T5.2.2 / verifyAuditHashChain). */
|
|
76
|
-
export function computeAuditRecordHash(envelope) {
|
|
77
|
-
return hashRecord({
|
|
78
|
-
eventId: envelope.eventId,
|
|
79
|
-
family: envelope.family,
|
|
80
|
-
plane: envelope.plane,
|
|
81
|
-
traceId: envelope.traceId,
|
|
82
|
-
sequence: envelope.sequence,
|
|
83
|
-
createdAt: envelope.createdAt,
|
|
84
|
-
payload: envelope.payload,
|
|
85
|
-
redaction: envelope.redaction,
|
|
86
|
-
previousHash: envelope.integrity.previousHash,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
export function buildAuditEnvelope(input) {
|
|
90
|
-
const { payload, redaction } = redactAuditEvent(input.payload);
|
|
91
|
-
const eventId = input.eventId ?? crypto.randomUUID();
|
|
92
|
-
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
93
|
-
const recordHash = hashRecord({
|
|
94
|
-
eventId,
|
|
95
|
-
family: input.family,
|
|
96
|
-
plane: input.plane,
|
|
97
|
-
traceId: input.traceId,
|
|
98
|
-
sequence: input.sequence,
|
|
99
|
-
createdAt,
|
|
100
|
-
payload,
|
|
101
|
-
redaction,
|
|
102
|
-
previousHash: input.previousHash,
|
|
103
|
-
});
|
|
104
|
-
return {
|
|
105
|
-
eventId,
|
|
106
|
-
family: input.family,
|
|
107
|
-
plane: input.plane,
|
|
108
|
-
traceId: input.traceId,
|
|
109
|
-
sequence: input.sequence,
|
|
110
|
-
createdAt,
|
|
111
|
-
payload,
|
|
112
|
-
redaction,
|
|
113
|
-
integrity: {
|
|
114
|
-
previousHash: input.previousHash,
|
|
115
|
-
recordHash,
|
|
116
|
-
schemaVersion: "observability.v5",
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
/** @internal Maps legacy field manifest to audit manifest for persistence helpers. */
|
|
121
|
-
export function auditManifestFromFieldManifest(manifest) {
|
|
122
|
-
return {
|
|
123
|
-
manifestId: manifest.id,
|
|
124
|
-
maskedPaths: manifest.maskedFields.map(fieldPathToAuditPath),
|
|
125
|
-
erasedPaths: manifest.erasedFields.map(fieldPathToAuditPath),
|
|
126
|
-
hashedPaths: manifest.hashedFields.map(fieldPathToAuditPath),
|
|
127
|
-
contentRefPaths: [],
|
|
128
|
-
sensitivity: mapSensitivity(manifest.sensitivityLevel),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Audit envelope construction + redaction for observability-system v5.
|
|
3
|
+
*
|
|
4
|
+
* Core logic: redact structured payloads, attach RedactionManifest (L0/L1 contract paths),
|
|
5
|
+
* compute hash-chain fields for append-only ledger rows.
|
|
6
|
+
*
|
|
7
|
+
* Dependencies: observability redactEvent (field-level); maps to envelope redaction paths.
|
|
8
|
+
*
|
|
9
|
+
* Boundaries: persistence is delegated to AppendOnlyAuditStore / DB adapters.
|
|
10
|
+
*
|
|
11
|
+
* Test coverage: tests/unit/observability/audit-envelope.test.ts
|
|
12
|
+
*/
|
|
13
|
+
import * as crypto from "node:crypto";
|
|
14
|
+
import { redactEvent } from "../redaction/manifest.js";
|
|
15
|
+
function fieldPathToAuditPath(field) {
|
|
16
|
+
if (field.startsWith("/")) {
|
|
17
|
+
return field;
|
|
18
|
+
}
|
|
19
|
+
return `/payload/${field.replace(/\./g, "/")}`;
|
|
20
|
+
}
|
|
21
|
+
function mapSensitivity(level) {
|
|
22
|
+
switch (level) {
|
|
23
|
+
case "public":
|
|
24
|
+
return "public";
|
|
25
|
+
case "internal":
|
|
26
|
+
return "internal";
|
|
27
|
+
case "confidential":
|
|
28
|
+
return "private";
|
|
29
|
+
case "restricted":
|
|
30
|
+
return "sensitive";
|
|
31
|
+
default:
|
|
32
|
+
return "internal";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Apply field redaction rules and lift manifests to audit path vocabulary (observability-system.detail §2).
|
|
37
|
+
*/
|
|
38
|
+
export function redactAuditEvent(payload) {
|
|
39
|
+
const { redacted, manifest } = redactEvent(payload);
|
|
40
|
+
const redaction = {
|
|
41
|
+
manifestId: manifest.id,
|
|
42
|
+
maskedPaths: manifest.maskedFields.map(fieldPathToAuditPath),
|
|
43
|
+
erasedPaths: manifest.erasedFields.map(fieldPathToAuditPath),
|
|
44
|
+
hashedPaths: manifest.hashedFields.map(fieldPathToAuditPath),
|
|
45
|
+
contentRefPaths: [],
|
|
46
|
+
sensitivity: mapSensitivity(manifest.sensitivityLevel),
|
|
47
|
+
};
|
|
48
|
+
return { payload: redacted, redaction };
|
|
49
|
+
}
|
|
50
|
+
function stableStringify(value) {
|
|
51
|
+
if (value === null || typeof value !== "object") {
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return `[${value.map((v) => stableStringify(v)).join(",")}]`;
|
|
56
|
+
}
|
|
57
|
+
const obj = value;
|
|
58
|
+
const keys = Object.keys(obj).sort();
|
|
59
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
60
|
+
}
|
|
61
|
+
function hashRecord(input) {
|
|
62
|
+
const canonical = stableStringify({
|
|
63
|
+
eventId: input.eventId,
|
|
64
|
+
family: input.family,
|
|
65
|
+
plane: input.plane,
|
|
66
|
+
traceId: input.traceId,
|
|
67
|
+
sequence: input.sequence,
|
|
68
|
+
createdAt: input.createdAt,
|
|
69
|
+
payload: input.payload,
|
|
70
|
+
redaction: input.redaction,
|
|
71
|
+
previousHash: input.previousHash ?? null,
|
|
72
|
+
});
|
|
73
|
+
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
74
|
+
}
|
|
75
|
+
/** Recompute integrity hash for an existing envelope (T5.2.2 / verifyAuditHashChain). */
|
|
76
|
+
export function computeAuditRecordHash(envelope) {
|
|
77
|
+
return hashRecord({
|
|
78
|
+
eventId: envelope.eventId,
|
|
79
|
+
family: envelope.family,
|
|
80
|
+
plane: envelope.plane,
|
|
81
|
+
traceId: envelope.traceId,
|
|
82
|
+
sequence: envelope.sequence,
|
|
83
|
+
createdAt: envelope.createdAt,
|
|
84
|
+
payload: envelope.payload,
|
|
85
|
+
redaction: envelope.redaction,
|
|
86
|
+
previousHash: envelope.integrity.previousHash,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function buildAuditEnvelope(input) {
|
|
90
|
+
const { payload, redaction } = redactAuditEvent(input.payload);
|
|
91
|
+
const eventId = input.eventId ?? crypto.randomUUID();
|
|
92
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
93
|
+
const recordHash = hashRecord({
|
|
94
|
+
eventId,
|
|
95
|
+
family: input.family,
|
|
96
|
+
plane: input.plane,
|
|
97
|
+
traceId: input.traceId,
|
|
98
|
+
sequence: input.sequence,
|
|
99
|
+
createdAt,
|
|
100
|
+
payload,
|
|
101
|
+
redaction,
|
|
102
|
+
previousHash: input.previousHash,
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
eventId,
|
|
106
|
+
family: input.family,
|
|
107
|
+
plane: input.plane,
|
|
108
|
+
traceId: input.traceId,
|
|
109
|
+
sequence: input.sequence,
|
|
110
|
+
createdAt,
|
|
111
|
+
payload,
|
|
112
|
+
redaction,
|
|
113
|
+
integrity: {
|
|
114
|
+
previousHash: input.previousHash,
|
|
115
|
+
recordHash,
|
|
116
|
+
schemaVersion: "observability.v5",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** @internal Maps legacy field manifest to audit manifest for persistence helpers. */
|
|
121
|
+
export function auditManifestFromFieldManifest(manifest) {
|
|
122
|
+
return {
|
|
123
|
+
manifestId: manifest.id,
|
|
124
|
+
maskedPaths: manifest.maskedFields.map(fieldPathToAuditPath),
|
|
125
|
+
erasedPaths: manifest.erasedFields.map(fieldPathToAuditPath),
|
|
126
|
+
hashedPaths: manifest.hashedFields.map(fieldPathToAuditPath),
|
|
127
|
+
contentRefPaths: [],
|
|
128
|
+
sensitivity: mapSensitivity(manifest.sensitivityLevel),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import type { AppendOnlyAuditStore } from "./append-only-audit-store.js";
|
|
2
|
-
import { type AuditEnvelope, type AuditEventFamily } from "./audit-envelope.js";
|
|
3
|
-
export interface AuditExportRange {
|
|
4
|
-
from: string;
|
|
5
|
-
to: string;
|
|
6
|
-
families?: AuditEventFamily[];
|
|
7
|
-
}
|
|
8
|
-
export type AuditHashChainVerificationStatus = "pass" | "broken" | "incomplete";
|
|
9
|
-
export interface AuditHashChainVerificationReport {
|
|
10
|
-
reportId: string;
|
|
11
|
-
generatedAt: string;
|
|
12
|
-
range: AuditExportRange;
|
|
13
|
-
checkedEventCount: number;
|
|
14
|
-
status: AuditHashChainVerificationStatus;
|
|
15
|
-
brokenAtEventIds: string[];
|
|
16
|
-
reasons: string[];
|
|
17
|
-
}
|
|
18
|
-
export interface VerifyAuditHashChainDeps {
|
|
19
|
-
loadRange(from: string, to: string, families?: AuditEventFamily[]): Promise<readonly AuditEnvelope<unknown>[]>;
|
|
20
|
-
}
|
|
21
|
-
export declare function verifyAuditHashChain(range: AuditExportRange, deps: VerifyAuditHashChainDeps): Promise<AuditHashChainVerificationReport>;
|
|
22
|
-
/** In-memory adapter: filter `AppendOnlyAuditStore.list()` by createdAt + optional families. */
|
|
23
|
-
export declare function createAppendOnlyAuditStoreRangeLoader(store: AppendOnlyAuditStore): VerifyAuditHashChainDeps;
|
|
1
|
+
import type { AppendOnlyAuditStore } from "./append-only-audit-store.js";
|
|
2
|
+
import { type AuditEnvelope, type AuditEventFamily } from "./audit-envelope.js";
|
|
3
|
+
export interface AuditExportRange {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
families?: AuditEventFamily[];
|
|
7
|
+
}
|
|
8
|
+
export type AuditHashChainVerificationStatus = "pass" | "broken" | "incomplete";
|
|
9
|
+
export interface AuditHashChainVerificationReport {
|
|
10
|
+
reportId: string;
|
|
11
|
+
generatedAt: string;
|
|
12
|
+
range: AuditExportRange;
|
|
13
|
+
checkedEventCount: number;
|
|
14
|
+
status: AuditHashChainVerificationStatus;
|
|
15
|
+
brokenAtEventIds: string[];
|
|
16
|
+
reasons: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface VerifyAuditHashChainDeps {
|
|
19
|
+
loadRange(from: string, to: string, families?: AuditEventFamily[]): Promise<readonly AuditEnvelope<unknown>[]>;
|
|
20
|
+
}
|
|
21
|
+
export declare function verifyAuditHashChain(range: AuditExportRange, deps: VerifyAuditHashChainDeps): Promise<AuditHashChainVerificationReport>;
|
|
22
|
+
/** In-memory adapter: filter `AppendOnlyAuditStore.list()` by createdAt + optional families. */
|
|
23
|
+
export declare function createAppendOnlyAuditStoreRangeLoader(store: AppendOnlyAuditStore): VerifyAuditHashChainDeps;
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Range-based hash-chain verification for append-only audit rows (T5.2.2 / INT-S3).
|
|
3
|
-
*
|
|
4
|
-
* Core logic: load events in [from, to], order by sequence, recompute recordHash and
|
|
5
|
-
* verify previousHash links between consecutive **loaded** rows only (partial ranges
|
|
6
|
-
* may start mid-chain; parent outside the slice is not validated). Empty or invalid
|
|
7
|
-
* ranges yield incomplete (T5.2.2 / task verification plan).
|
|
8
|
-
*
|
|
9
|
-
* Dependencies: computeAuditRecordHash from audit-envelope; callers supply loadRange via deps.
|
|
10
|
-
*
|
|
11
|
-
* Test coverage: tests/unit/observability/verify-audit-hash-chain.test.ts
|
|
12
|
-
*/
|
|
13
|
-
import * as crypto from "node:crypto";
|
|
14
|
-
import { computeAuditRecordHash } from "./audit-envelope.js";
|
|
15
|
-
function unique(ids) {
|
|
16
|
-
return [...new Set(ids)];
|
|
17
|
-
}
|
|
18
|
-
export async function verifyAuditHashChain(range, deps) {
|
|
19
|
-
const generatedAt = new Date().toISOString();
|
|
20
|
-
const reportId = crypto.randomUUID();
|
|
21
|
-
if (range.from > range.to) {
|
|
22
|
-
return {
|
|
23
|
-
reportId,
|
|
24
|
-
generatedAt,
|
|
25
|
-
range,
|
|
26
|
-
checkedEventCount: 0,
|
|
27
|
-
status: "incomplete",
|
|
28
|
-
brokenAtEventIds: [],
|
|
29
|
-
reasons: ["invalid_range_from_after_to"],
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
const raw = await deps.loadRange(range.from, range.to, range.families);
|
|
33
|
-
const events = [...raw].sort((a, b) => a.sequence - b.sequence);
|
|
34
|
-
if (events.length === 0) {
|
|
35
|
-
return {
|
|
36
|
-
reportId,
|
|
37
|
-
generatedAt,
|
|
38
|
-
range,
|
|
39
|
-
checkedEventCount: 0,
|
|
40
|
-
status: "incomplete",
|
|
41
|
-
brokenAtEventIds: [],
|
|
42
|
-
reasons: ["range_empty"],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const brokenAtEventIds = [];
|
|
46
|
-
for (let i = 0; i < events.length; i += 1) {
|
|
47
|
-
const event = events[i];
|
|
48
|
-
const expected = computeAuditRecordHash(event);
|
|
49
|
-
if (event.integrity.recordHash !== expected) {
|
|
50
|
-
brokenAtEventIds.push(event.eventId);
|
|
51
|
-
}
|
|
52
|
-
const prev = events[i - 1];
|
|
53
|
-
if (prev && event.integrity.previousHash !== prev.integrity.recordHash) {
|
|
54
|
-
brokenAtEventIds.push(event.eventId);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const uniq = unique(brokenAtEventIds);
|
|
58
|
-
const broken = uniq.length > 0;
|
|
59
|
-
return {
|
|
60
|
-
reportId,
|
|
61
|
-
generatedAt,
|
|
62
|
-
range,
|
|
63
|
-
checkedEventCount: events.length,
|
|
64
|
-
status: broken ? "broken" : "pass",
|
|
65
|
-
brokenAtEventIds: uniq,
|
|
66
|
-
reasons: broken ? ["hash_chain_broken"] : ["hash_chain_valid"],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/** In-memory adapter: filter `AppendOnlyAuditStore.list()` by createdAt + optional families. */
|
|
70
|
-
export function createAppendOnlyAuditStoreRangeLoader(store) {
|
|
71
|
-
return {
|
|
72
|
-
async loadRange(from, to, families) {
|
|
73
|
-
const fams = families?.length ? new Set(families) : undefined;
|
|
74
|
-
return store.list().filter((e) => {
|
|
75
|
-
if (e.createdAt < from || e.createdAt > to)
|
|
76
|
-
return false;
|
|
77
|
-
if (fams && !fams.has(e.family))
|
|
78
|
-
return false;
|
|
79
|
-
return true;
|
|
80
|
-
});
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Range-based hash-chain verification for append-only audit rows (T5.2.2 / INT-S3).
|
|
3
|
+
*
|
|
4
|
+
* Core logic: load events in [from, to], order by sequence, recompute recordHash and
|
|
5
|
+
* verify previousHash links between consecutive **loaded** rows only (partial ranges
|
|
6
|
+
* may start mid-chain; parent outside the slice is not validated). Empty or invalid
|
|
7
|
+
* ranges yield incomplete (T5.2.2 / task verification plan).
|
|
8
|
+
*
|
|
9
|
+
* Dependencies: computeAuditRecordHash from audit-envelope; callers supply loadRange via deps.
|
|
10
|
+
*
|
|
11
|
+
* Test coverage: tests/unit/observability/verify-audit-hash-chain.test.ts
|
|
12
|
+
*/
|
|
13
|
+
import * as crypto from "node:crypto";
|
|
14
|
+
import { computeAuditRecordHash } from "./audit-envelope.js";
|
|
15
|
+
function unique(ids) {
|
|
16
|
+
return [...new Set(ids)];
|
|
17
|
+
}
|
|
18
|
+
export async function verifyAuditHashChain(range, deps) {
|
|
19
|
+
const generatedAt = new Date().toISOString();
|
|
20
|
+
const reportId = crypto.randomUUID();
|
|
21
|
+
if (range.from > range.to) {
|
|
22
|
+
return {
|
|
23
|
+
reportId,
|
|
24
|
+
generatedAt,
|
|
25
|
+
range,
|
|
26
|
+
checkedEventCount: 0,
|
|
27
|
+
status: "incomplete",
|
|
28
|
+
brokenAtEventIds: [],
|
|
29
|
+
reasons: ["invalid_range_from_after_to"],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const raw = await deps.loadRange(range.from, range.to, range.families);
|
|
33
|
+
const events = [...raw].sort((a, b) => a.sequence - b.sequence);
|
|
34
|
+
if (events.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
reportId,
|
|
37
|
+
generatedAt,
|
|
38
|
+
range,
|
|
39
|
+
checkedEventCount: 0,
|
|
40
|
+
status: "incomplete",
|
|
41
|
+
brokenAtEventIds: [],
|
|
42
|
+
reasons: ["range_empty"],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const brokenAtEventIds = [];
|
|
46
|
+
for (let i = 0; i < events.length; i += 1) {
|
|
47
|
+
const event = events[i];
|
|
48
|
+
const expected = computeAuditRecordHash(event);
|
|
49
|
+
if (event.integrity.recordHash !== expected) {
|
|
50
|
+
brokenAtEventIds.push(event.eventId);
|
|
51
|
+
}
|
|
52
|
+
const prev = events[i - 1];
|
|
53
|
+
if (prev && event.integrity.previousHash !== prev.integrity.recordHash) {
|
|
54
|
+
brokenAtEventIds.push(event.eventId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const uniq = unique(brokenAtEventIds);
|
|
58
|
+
const broken = uniq.length > 0;
|
|
59
|
+
return {
|
|
60
|
+
reportId,
|
|
61
|
+
generatedAt,
|
|
62
|
+
range,
|
|
63
|
+
checkedEventCount: events.length,
|
|
64
|
+
status: broken ? "broken" : "pass",
|
|
65
|
+
brokenAtEventIds: uniq,
|
|
66
|
+
reasons: broken ? ["hash_chain_broken"] : ["hash_chain_valid"],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** In-memory adapter: filter `AppendOnlyAuditStore.list()` by createdAt + optional families. */
|
|
70
|
+
export function createAppendOnlyAuditStoreRangeLoader(store) {
|
|
71
|
+
return {
|
|
72
|
+
async loadRange(from, to, families) {
|
|
73
|
+
const fams = families?.length ? new Set(families) : undefined;
|
|
74
|
+
return store.list().filter((e) => {
|
|
75
|
+
if (e.createdAt < from || e.createdAt > to)
|
|
76
|
+
return false;
|
|
77
|
+
if (fams && !fams.has(e.family))
|
|
78
|
+
return false;
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|