@asaidimu/hestia 1.0.0

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.
@@ -0,0 +1,147 @@
1
+ import type { QueryDSL } from "@asaidimu/query"
2
+ import { HestiaNetworkClient } from "../../core/client"
3
+ import type { Page, PaginationInfo } from "../../core/types"
4
+ import type { AuditEntry } from "./types"
5
+
6
+ interface FindEnvelope {
7
+ data: AuditEntry[]
8
+ metadata?: { page?: PaginationInfo }
9
+ }
10
+
11
+ interface StreamDoc {
12
+ data: AuditEntry
13
+ }
14
+
15
+ export class HestiaLogs {
16
+ constructor(
17
+ private client: HestiaNetworkClient,
18
+ private baseUrl: string,
19
+ ) {}
20
+
21
+ async find(query?: QueryDSL<AuditEntry>): Promise<Page<AuditEntry>> {
22
+ const res = await this.client.post<FindEnvelope>(
23
+ "/system/audit/log/query",
24
+ { query: query ?? {} },
25
+ )
26
+
27
+ const items = res.data?.data ?? []
28
+ const pageMeta = res.data?.metadata?.page ?? {
29
+ number: 1,
30
+ size: items.length,
31
+ count: items.length,
32
+ total: items.length,
33
+ pages: 1,
34
+ }
35
+
36
+ return { data: items, loading: false, page: pageMeta, error: null }
37
+ }
38
+
39
+ async list(): Promise<Page<AuditEntry>> {
40
+ return this.find({
41
+ pagination: { type: "offset", offset: 0, limit: 20 },
42
+ })
43
+ }
44
+
45
+ getStreamUrl(): string {
46
+ return `${this.baseUrl}/system/audit/log/stream`
47
+ }
48
+
49
+ subscribe(onEntry: (entry: AuditEntry) => void): () => void {
50
+ const url = this.getStreamUrl()
51
+ const eventSource = new EventSource(url, { withCredentials: true })
52
+
53
+ eventSource.onmessage = (event) => {
54
+ try {
55
+ const parsed = JSON.parse(event.data) as StreamDoc
56
+ if (parsed?.data) {
57
+ onEntry(parsed.data)
58
+ }
59
+ } catch {
60
+ // skip non-JSON lines
61
+ }
62
+ }
63
+
64
+ eventSource.onerror = () => {
65
+ eventSource.close()
66
+ }
67
+
68
+ return () => {
69
+ eventSource.close()
70
+ }
71
+ }
72
+
73
+ stream(): {
74
+ stream: () => AsyncIterable<AuditEntry>
75
+ cancel: () => void
76
+ status: () => "active" | "cancelled" | "completed"
77
+ } {
78
+ const url = this.getStreamUrl()
79
+ let eventSource: EventSource | null = null
80
+ let currentStatus: "active" | "cancelled" | "completed" = "active"
81
+ let pendingResolve: (() => void) | null = null
82
+
83
+ const asyncStream = async function* () {
84
+ const pending: AuditEntry[] = []
85
+
86
+ eventSource = new EventSource(url, { withCredentials: true })
87
+
88
+ eventSource.onmessage = (event) => {
89
+ for (const line of event.data.split("\n")) {
90
+ const trimmed = line.trim()
91
+ if (!trimmed) continue
92
+ try {
93
+ const parsed = JSON.parse(trimmed) as StreamDoc
94
+ if (parsed?.data) pending.push(parsed.data)
95
+ } catch {
96
+ // skip non-JSON lines
97
+ }
98
+ }
99
+ if (pending.length > 0 && pendingResolve) {
100
+ pendingResolve()
101
+ pendingResolve = null
102
+ }
103
+ }
104
+
105
+ eventSource.onerror = () => {
106
+ if (currentStatus === "active") currentStatus = "completed"
107
+ if (pendingResolve) {
108
+ pendingResolve()
109
+ pendingResolve = null
110
+ }
111
+ }
112
+
113
+ try {
114
+ while (currentStatus === "active") {
115
+ if (pending.length > 0) {
116
+ yield pending.shift()!
117
+ } else {
118
+ await new Promise<void>((resolve) => {
119
+ pendingResolve = resolve
120
+ if (pending.length > 0) {
121
+ resolve()
122
+ pendingResolve = null
123
+ }
124
+ })
125
+ }
126
+ }
127
+ } finally {
128
+ eventSource?.close()
129
+ if (pendingResolve) {
130
+ pendingResolve()
131
+ pendingResolve = null
132
+ }
133
+ if (currentStatus === "active") currentStatus = "completed"
134
+ }
135
+ }
136
+
137
+ return {
138
+ stream: () => asyncStream(),
139
+ cancel: () => {
140
+ if (currentStatus !== "active") return
141
+ currentStatus = "cancelled"
142
+ eventSource?.close()
143
+ },
144
+ status: () => currentStatus,
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,38 @@
1
+ export interface AuditEntry {
2
+ event_id: string
3
+ occurred_at: string
4
+ recorded_at: string
5
+ trace_id?: string
6
+ request_id?: string
7
+ actor_id: string
8
+ actor_type: "user" | "service" | "system" | "anonymous"
9
+ on_behalf_of_id?: string
10
+ auth_method?: "password" | "oauth" | "api_key" | "mtls" | "sso" | "service_account" | "none"
11
+ session_id?: string
12
+ operation: "create" | "read" | "update" | "delete" | "login" | "logout" | "grant" | "revoke" | "execute" | "other"
13
+ resource_type: string
14
+ resource_id?: string
15
+ event_name: string
16
+ status: "success" | "failure" | "denied" | "error"
17
+ severity?: "info" | "warning" | "critical"
18
+ error_code?: string
19
+ error_message?: string
20
+ latency_ms: number
21
+ source_ip?: string
22
+ user_agent?: string
23
+ service_name: string
24
+ region?: string
25
+ }
26
+
27
+ export type RequestLogEntry = AuditEntry
28
+
29
+ export interface LogFilter {
30
+ actor_id?: string
31
+ actor_type?: string
32
+ operation?: string
33
+ status?: string
34
+ resource_type?: string
35
+ trace_id?: string
36
+ start?: string
37
+ end?: string
38
+ }
@@ -0,0 +1,74 @@
1
+ import type { QueryDSL } from "@asaidimu/query"
2
+ import { HestiaNetworkClient } from "../../core/client"
3
+ import { HestiaCollection } from "../../core/collection"
4
+ import type { Document, Page } from "../../core/types"
5
+ import type {
6
+ PolicyOperation,
7
+ PolicyRule,
8
+ UpsertOperationRequest,
9
+ UpsertRuleRequest,
10
+ } from "./types"
11
+
12
+ export class HestiaPolicies {
13
+ private operations: HestiaCollection<PolicyOperation>
14
+ private rules: HestiaCollection<PolicyRule>
15
+
16
+ constructor(private client: HestiaNetworkClient) {
17
+ this.operations = new HestiaCollection<PolicyOperation>(client, "_policy_operation_")
18
+ this.rules = new HestiaCollection<PolicyRule>(client, "_policy_rule_")
19
+ }
20
+
21
+ // ── Operations ──
22
+
23
+ async findOperations(query?: QueryDSL<PolicyOperation>): Promise<Page<PolicyOperation>> {
24
+ return this.operations.find(query)
25
+ }
26
+
27
+ async listOperations(): Promise<Page<PolicyOperation>> {
28
+ return this.operations.find({ pagination: { type: "offset", offset: 0, limit: 50 } })
29
+ }
30
+
31
+ async readOperation(name: string): Promise<Document<PolicyOperation> | undefined> {
32
+ return this.operations.read(name)
33
+ }
34
+
35
+ async createOperation(data: UpsertOperationRequest): Promise<Document<PolicyOperation>> {
36
+ return this.operations.create(data as any)
37
+ }
38
+
39
+ async updateOperation(name: string, data: UpsertOperationRequest): Promise<Document<PolicyOperation>> {
40
+ return this.operations.update(name, data as any)
41
+ }
42
+
43
+ async deleteOperation(name: string): Promise<void> {
44
+ return this.operations.delete(name)
45
+ }
46
+
47
+ // ── Rules ──
48
+
49
+ async findRules(query?: QueryDSL<PolicyRule>): Promise<Page<PolicyRule>> {
50
+ return this.rules.find(query)
51
+ }
52
+
53
+ async listRules(): Promise<Page<PolicyRule>> {
54
+ return this.rules.find({ pagination: { type: "offset", offset: 0, limit: 50 } })
55
+ }
56
+
57
+ async readRule(name: string): Promise<Document<PolicyRule> | undefined> {
58
+ return this.rules.read(name)
59
+ }
60
+
61
+ async createRule(data: UpsertRuleRequest): Promise<Document<PolicyRule>> {
62
+ return this.rules.create(data as any)
63
+ }
64
+
65
+ async updateRule(name: string, data: UpsertRuleRequest): Promise<Document<PolicyRule>> {
66
+ return this.rules.update(name, data as any)
67
+ }
68
+
69
+ async deleteRule(name: string): Promise<void> {
70
+ return this.rules.delete(name)
71
+ }
72
+
73
+
74
+ }
@@ -0,0 +1,43 @@
1
+ export interface PolicyOperation {
2
+ name: string
3
+ ruleKey: string
4
+ description?: string
5
+ intentType?: "COMMAND" | "QUERY"
6
+ }
7
+
8
+ export interface UpsertOperationRequest {
9
+ ruleKey: string
10
+ description?: string
11
+ intentType?: "COMMAND" | "QUERY"
12
+ }
13
+
14
+ export interface PolicyRule {
15
+ name: string
16
+ ruleType?: string
17
+ expression?: string
18
+ description?: string
19
+ }
20
+
21
+ export interface UpsertRuleRequest {
22
+ expression: string
23
+ ruleType?: string
24
+ description?: string
25
+ }
26
+
27
+ export interface ValidateRuleRequest {
28
+ expression: string
29
+ identity?: Record<string, unknown>
30
+ resource?: Record<string, unknown>
31
+ environment?: Record<string, unknown>
32
+ }
33
+
34
+ export interface ValidateRuleResult {
35
+ valid: boolean
36
+ compile?: string
37
+ result?: boolean
38
+ }
39
+
40
+ export interface ReloadResult {
41
+ operations: number
42
+ rules: number
43
+ }
package/test-setup.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { spawn, type ChildProcess } from "child_process"
2
+ import { resolve } from "path"
3
+
4
+ const PROJECT_ROOT = resolve(import.meta.dirname, "..")
5
+ const SERVER_BIN = resolve(PROJECT_ROOT, "test-server")
6
+ const START_TIMEOUT = 30000
7
+
8
+ let serverProc: ChildProcess | null = null
9
+
10
+ function sleep(ms: number): Promise<void> {
11
+ return new Promise((r) => setTimeout(r, ms))
12
+ }
13
+
14
+ export async function setup(): Promise<void> {
15
+ await new Promise<void>((resolvePromise, reject) => {
16
+ serverProc = spawn(SERVER_BIN, [], {
17
+ cwd: PROJECT_ROOT,
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ })
20
+
21
+ let started = false
22
+ const onData = (data: Buffer) => {
23
+ const text = data.toString().trim()
24
+ if (!started && /^\d+$/.test(text)) {
25
+ started = true
26
+ resolvePromise()
27
+ }
28
+ }
29
+
30
+ serverProc.stdout!.on("data", onData)
31
+ serverProc.stderr!.on("data", () => {})
32
+
33
+ serverProc.on("error", (err) => {
34
+ if (!started) reject(err)
35
+ })
36
+ serverProc.on("exit", (code) => {
37
+ if (!started) reject(new Error(`server exited with code ${code} before ready`))
38
+ })
39
+
40
+ setTimeout(() => {
41
+ if (!started) reject(new Error("server start timeout"))
42
+ }, START_TIMEOUT)
43
+ })
44
+
45
+ await sleep(1000)
46
+ }
47
+
48
+ export async function teardown(): Promise<void> {
49
+ if (serverProc && !serverProc.killed) {
50
+ serverProc.kill("SIGTERM")
51
+ serverProc = null
52
+ }
53
+ }
package/utils/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./pager"
package/utils/pager.ts ADDED
@@ -0,0 +1,226 @@
1
+ import { QueryDSL, QueryFilter, SortConfiguration, createMatcher, createSorter } from '@asaidimu/query'; //
2
+ import { DELETE_SYMBOL, ReactiveDataStore } from "@asaidimu/utils-store"; //
3
+ import { Page, PagedData } from '../core/types';
4
+
5
+ export interface FetchResult<T extends Record<string, any>> {
6
+ data: T[];
7
+ replace: boolean;
8
+ scope: 'page' | 'collection';
9
+ total?: number;
10
+ }
11
+
12
+ // Extensible parameters object embracing your DataStore infra
13
+ export interface ArrayPagerOptions<T extends Record<string, any>, F = any> {
14
+ collectionName: string; // Unique identifier used to partition the cache matrix
15
+ initialData?: T[];
16
+ page?: number;
17
+ size?: number;
18
+ customFunctions?: Record<string, (...args: any[]) => boolean>;
19
+ fetch?: (
20
+ query: QueryDSL<T, F>
21
+ ) => Promise<FetchResult<T>> | FetchResult<T>;
22
+ }
23
+
24
+ export function createArrayPager<T extends Record<string, any>, F = any>(
25
+ options: ArrayPagerOptions<T, F>
26
+ ): PagedData<T> {
27
+ const CACHE_KEY = `${options.collectionName}_array_pager_state_`; //
28
+ const store = new ReactiveDataStore({})
29
+
30
+ // Baseline backing arrays managed by this adapter instance
31
+ let pristineData = [...(options.initialData ?? [])];
32
+ let activeScope: 'page' | 'collection' = 'collection';
33
+ let remoteTotalRecords: number | undefined = undefined;
34
+
35
+ // Track active filter parameters
36
+ let currentPageNum = options.page ?? 1;
37
+ let currentPageSize = options.size ?? 20; // Defaulting to 20 per your page controller sentinel
38
+ let currentSort: SortConfiguration<T>[] = [];
39
+ let currentFilter: QueryFilter<T> | undefined = undefined;
40
+
41
+ // Tracker for errors across async horizons
42
+ let operationalError: any = undefined;
43
+
44
+ const { match } = createMatcher<T, F>(options.customFunctions ?? {}); //
45
+ const { sort: localSort} = createSorter()
46
+ const selector = store.select((s: any) => s[CACHE_KEY]); //
47
+
48
+ // Default fallback layout conforming strictly to Page<T>
49
+ const sentinel: Page<T> = { //
50
+ data: [], //
51
+ loading: true, //
52
+ error: undefined,
53
+ page: { //
54
+ number: 1, //
55
+ size: currentPageSize, //
56
+ count: 0, //
57
+ total: 0, //
58
+ pages: 1, //
59
+ },
60
+ };
61
+
62
+ /**
63
+ * Constructs the current active QueryDSL footprint.
64
+ */
65
+ const buildQueryDSL = (): QueryDSL<T, F> => ({ //
66
+ filters: currentFilter, //
67
+ sort: currentSort, //
68
+ pagination: { //
69
+ type: 'offset', //
70
+ offset: (currentPageNum - 1) * currentPageSize, //
71
+ limit: currentPageSize //
72
+ }
73
+ });
74
+
75
+ /**
76
+ * Orchestrates the fetching lifecycle and syncs results into the centralized store.
77
+ */
78
+ const executeDataPipeline = async (isLoadingFallback = true) => {
79
+ if (options.fetch) {
80
+ operationalError = undefined;
81
+
82
+ if (isLoadingFallback) {
83
+ // Broadcast transitional loading state via the central store
84
+ const previousState = selector.get() ?? sentinel;
85
+ await store.set({
86
+ [CACHE_KEY]: { ...previousState, loading: true, error: undefined }
87
+ });
88
+ }
89
+
90
+ try {
91
+ const queryDSL = buildQueryDSL();
92
+ const result = await options.fetch(queryDSL);
93
+
94
+ activeScope = result.scope;
95
+ remoteTotalRecords = result.total;
96
+
97
+ pristineData = result.replace
98
+ ? [...result.data]
99
+ : [...pristineData, ...result.data];
100
+ } catch (error) {
101
+ operationalError = error;
102
+ }
103
+ }
104
+
105
+ // Process and dispatch to store
106
+ const updatedPagePayload = computeProcessedPage(options.fetch ? isLoadingFallback : false);
107
+ await store.set({ [CACHE_KEY]: updatedPagePayload }); //
108
+ };
109
+
110
+ /**
111
+ * Coordinates local filtering, sorting and slicing transformations
112
+ * based on the active structural scope strategy.
113
+ */
114
+ const computeProcessedPage = (isLoadingFlag: boolean): Page<T> => { //
115
+ let records = [...pristineData];
116
+
117
+ if (activeScope === 'collection') {
118
+ // Execute standard local processing passes over the full source collection
119
+ if (currentFilter) {
120
+ records = records.filter((item) => match(item, currentFilter!)); //
121
+ }
122
+ if (currentSort.length > 0) {
123
+ records = localSort(records, currentSort); //
124
+ }
125
+
126
+ const total = records.length;
127
+ const pages = Math.ceil(total / currentPageSize) || 1;
128
+
129
+ if (currentPageNum > pages) currentPageNum = pages;
130
+ if (currentPageNum < 1) currentPageNum = 1;
131
+
132
+ const startIdx = (currentPageNum - 1) * currentPageSize;
133
+ const slicedData = records.slice(startIdx, startIdx + currentPageSize);
134
+
135
+ return {
136
+ data: slicedData as any,
137
+ loading: isLoadingFlag,
138
+ error: operationalError,
139
+ page: { //
140
+ number: currentPageNum,
141
+ size: currentPageSize,
142
+ count: slicedData.length,
143
+ total,
144
+ pages,
145
+ }
146
+ };
147
+ }
148
+
149
+ // Server-Driven mode: Data array has already been shaped for this target viewport
150
+ const total = remoteTotalRecords ?? records.length;
151
+ const pages = Math.ceil(total / currentPageSize) || 1;
152
+
153
+ return {
154
+ data: records as any,
155
+ loading: isLoadingFlag,
156
+ error: operationalError,
157
+ page: { //
158
+ number: currentPageNum,
159
+ size: currentPageSize,
160
+ count: records.length,
161
+ total,
162
+ pages,
163
+ }
164
+ };
165
+ };
166
+
167
+ // Kickstart immediate bootstrapping pass
168
+ executeDataPipeline(true);
169
+
170
+ return {
171
+ page: () => selector.get() ?? sentinel, //
172
+
173
+ navigate: async (page: number) => {
174
+ if (page < 1) throw new Error("Page number must be >= 1"); //
175
+ currentPageNum = page;
176
+ await executeDataPipeline(true);
177
+ },
178
+
179
+ resize: async (size: number, page: number) => {
180
+ if (size < 1) throw new Error("Page size must be >= 1"); //
181
+ currentPageSize = size;
182
+ currentPageNum = page;
183
+ await executeDataPipeline(true);
184
+ },
185
+
186
+ sort: async (sortConfig) => {
187
+ currentSort = Array.isArray(sortConfig) ? sortConfig : [sortConfig];
188
+ currentPageNum = 1; // Reset window index
189
+ await executeDataPipeline(true);
190
+ },
191
+
192
+ filter: async (queryFilter) => {
193
+ currentFilter = queryFilter;
194
+ currentPageNum = 1; // Reset window index
195
+ await executeDataPipeline(true);
196
+ },
197
+
198
+ refresh: async (delay: number = 600) => { //
199
+ const previousState = selector.get() ?? sentinel;
200
+ await store.set({
201
+ [CACHE_KEY]: { ...previousState, loading: true } //
202
+ });
203
+
204
+ await Promise.all([
205
+ new Promise((resolve) => setTimeout(resolve, delay)), //
206
+ executeDataPipeline(false) // Handle full resolution pipeline passing down false to bypass flashing double mutations
207
+ ]);
208
+
209
+ const finishedState = selector.get() ?? sentinel;
210
+ await store.set({
211
+ [CACHE_KEY]: { ...finishedState, loading: false } //
212
+ });
213
+ },
214
+
215
+ subscribe: (listener) => {
216
+ return selector.subscribe((state) => { //
217
+ listener(state); //
218
+ });
219
+ },
220
+
221
+ invalidate: async () => {
222
+ await store.set({ [CACHE_KEY]: DELETE_SYMBOL }); //
223
+ pristineData = [];
224
+ },
225
+ };
226
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config"
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globalSetup: ["./test-setup.ts"],
6
+ fileParallelism: false,
7
+ },
8
+ })