@asaidimu/hestia 1.0.0 → 1.0.1

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.
@@ -1,74 +1,177 @@
1
1
  import type { QueryDSL } from "@asaidimu/query"
2
2
  import { HestiaNetworkClient } from "../../core/client"
3
- import { HestiaCollection } from "../../core/collection"
4
- import type { Document, Page } from "../../core/types"
3
+ import { ReactiveDataStore } from "@asaidimu/utils-store"
4
+ import { createPagedController } from "../../core/pager"
5
+ import type { Document, Page, PagedData, PaginationInfo, StoreEvent } from "../../core/types"
6
+ import type { DocumentStore } from "../../core/types"
5
7
  import type {
6
- PolicyOperation,
7
- PolicyRule,
8
+ OperationPolicy,
9
+ IamRule,
8
10
  UpsertOperationRequest,
9
11
  UpsertRuleRequest,
12
+ ReloadResult,
10
13
  } from "./types"
11
14
 
12
- export class HestiaPolicies {
13
- private operations: HestiaCollection<PolicyOperation>
14
- private rules: HestiaCollection<PolicyRule>
15
+ const OPERATIONS_COLLECTION = "_operation_policy_"
16
+ const RULES_COLLECTION = "_iam_rule_"
17
+
18
+ export class HestiaPolicies implements DocumentStore<OperationPolicy, QueryDSL<OperationPolicy>, string, QueryDSL<OperationPolicy>, Record<string, unknown>, string, string, Record<string, unknown>> {
19
+ private pager: PagedData<OperationPolicy>
20
+ private operationsPath = "/system/policies/operation"
21
+ private rulesPath = "/system/policies/rule"
15
22
 
16
23
  constructor(private client: HestiaNetworkClient) {
17
- this.operations = new HestiaCollection<PolicyOperation>(client, "_policy_operation_")
18
- this.rules = new HestiaCollection<PolicyRule>(client, "_policy_rule_")
24
+ this.pager = createPagedController<OperationPolicy>(
25
+ "policies",
26
+ new ReactiveDataStore<any>({}),
27
+ {},
28
+ (query) => this.find(query),
29
+ )
19
30
  }
20
31
 
21
- // ── Operations ──
32
+ private async collectionQuery<T extends Record<string, any>>(
33
+ collection: string,
34
+ query?: Record<string, unknown>,
35
+ ): Promise<Page<T>> {
36
+ const res = await this.client.post<{
37
+ data: Document<T>[];
38
+ metadata?: { page?: PaginationInfo };
39
+ }>(`/system/collections/document/${encodeURIComponent(collection)}/query`, query ?? {})
40
+ const items = res.data?.data ?? []
41
+ const pageMeta = res.data?.metadata?.page ?? {
42
+ number: 1,
43
+ size: items.length,
44
+ count: items.length,
45
+ total: items.length,
46
+ pages: 1,
47
+ }
48
+ return { data: items, loading: false, page: pageMeta, error: null }
49
+ }
22
50
 
23
- async findOperations(query?: QueryDSL<PolicyOperation>): Promise<Page<PolicyOperation>> {
24
- return this.operations.find(query)
51
+ async find(query?: QueryDSL<OperationPolicy>): Promise<Page<OperationPolicy>> {
52
+ return this.collectionQuery<OperationPolicy>(OPERATIONS_COLLECTION, query as Record<string, unknown> | undefined)
25
53
  }
26
54
 
27
- async listOperations(): Promise<Page<PolicyOperation>> {
28
- return this.operations.find({ pagination: { type: "offset", offset: 0, limit: 50 } })
55
+ async list(options?: QueryDSL<OperationPolicy>): Promise<Page<OperationPolicy>> {
56
+ return this.find(options ?? { pagination: { type: "offset", offset: 0, limit: 50 } })
29
57
  }
30
58
 
31
- async readOperation(name: string): Promise<Document<PolicyOperation> | undefined> {
32
- return this.operations.read(name)
59
+ async read(id: string): Promise<Document<OperationPolicy> | undefined> {
60
+ try {
61
+ const res = await this.client.get<{ data: Document<OperationPolicy> }>(
62
+ `${this.operationsPath}/${encodeURIComponent(id)}`,
63
+ )
64
+ return res.data?.data
65
+ } catch (err: any) {
66
+ if (err?.code === "SYNC-001-NF" || err?.code === "NOT_FOUND" || err?.code === "INTERNAL_ERROR" && typeof err?.message === "string" && err.message.includes("not found")) return undefined
67
+ throw err
68
+ }
33
69
  }
34
70
 
35
- async createOperation(data: UpsertOperationRequest): Promise<Document<PolicyOperation>> {
36
- return this.operations.create(data as any)
71
+ async create(props: { data: Partial<OperationPolicy> }): Promise<Document<OperationPolicy> | undefined> {
72
+ const name = props.data.name
73
+ if (!name) throw new Error("Operation name is required for create")
74
+ const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
75
+ `${this.operationsPath}/${encodeURIComponent(name)}`,
76
+ props.data as UpsertOperationRequest,
77
+ )
78
+ return res.data!.data
37
79
  }
38
80
 
39
- async updateOperation(name: string, data: UpsertOperationRequest): Promise<Document<PolicyOperation>> {
40
- return this.operations.update(name, data as any)
81
+ async update(props: { data: Partial<OperationPolicy>; options?: string }): Promise<Document<OperationPolicy> | undefined> {
82
+ const name = props.options!
83
+ if (!name) throw new Error("Operation name is required for update")
84
+ const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
85
+ `${this.operationsPath}/${encodeURIComponent(name)}`,
86
+ props.data as UpsertOperationRequest,
87
+ )
88
+ return res.data!.data
41
89
  }
42
90
 
43
- async deleteOperation(name: string): Promise<void> {
44
- return this.operations.delete(name)
91
+ async delete(id: string): Promise<void> {
92
+ await this.client.delete(`${this.operationsPath}/${encodeURIComponent(id)}`)
45
93
  }
46
94
 
47
- // ── Rules ──
95
+ async upsertOperation(
96
+ name: string,
97
+ data: UpsertOperationRequest,
98
+ ): Promise<Document<OperationPolicy>> {
99
+ const res = await this.client.patch<{ data: Document<OperationPolicy> }>(
100
+ `${this.operationsPath}/${encodeURIComponent(name)}`,
101
+ data,
102
+ )
103
+ return res.data!.data
104
+ }
48
105
 
49
- async findRules(query?: QueryDSL<PolicyRule>): Promise<Page<PolicyRule>> {
50
- return this.rules.find(query)
106
+ async getRule(name: string): Promise<Document<IamRule> | undefined> {
107
+ try {
108
+ const res = await this.client.get<{ data: Document<IamRule> }>(
109
+ `${this.rulesPath}/${encodeURIComponent(name)}`,
110
+ )
111
+ return res.data?.data
112
+ } catch (err: any) {
113
+ if (err?.code === "SYNC-001-NF" || err?.code === "NOT_FOUND" || err?.code === "INTERNAL_ERROR" && typeof err?.message === "string" && err.message.includes("not found")) return undefined
114
+ throw err
115
+ }
51
116
  }
52
117
 
53
- async listRules(): Promise<Page<PolicyRule>> {
54
- return this.rules.find({ pagination: { type: "offset", offset: 0, limit: 50 } })
118
+ async upsertRule(
119
+ name: string,
120
+ data: UpsertRuleRequest,
121
+ ): Promise<Document<IamRule>> {
122
+ const res = await this.client.patch<{ data: Document<IamRule> }>(
123
+ `${this.rulesPath}/${encodeURIComponent(name)}`,
124
+ data,
125
+ )
126
+ return res.data!.data
55
127
  }
56
128
 
57
- async readRule(name: string): Promise<Document<PolicyRule> | undefined> {
58
- return this.rules.read(name)
129
+ async deleteRule(name: string): Promise<void> {
130
+ await this.client.delete(`${this.rulesPath}/${encodeURIComponent(name)}`)
59
131
  }
60
132
 
61
- async createRule(data: UpsertRuleRequest): Promise<Document<PolicyRule>> {
62
- return this.rules.create(data as any)
133
+ async listRules(options?: Record<string, unknown>): Promise<Page<IamRule>> {
134
+ return this.collectionQuery<IamRule>(RULES_COLLECTION, options)
63
135
  }
64
136
 
65
- async updateRule(name: string, data: UpsertRuleRequest): Promise<Document<PolicyRule>> {
66
- return this.rules.update(name, data as any)
137
+ async validateRule(
138
+ expression: string,
139
+ ): Promise<boolean> {
140
+ const res = await this.client.post<{ data: { valid: boolean } }>(
141
+ `${this.rulesPath}/validate`,
142
+ { expression },
143
+ )
144
+ return res.data?.data?.valid ?? false
67
145
  }
68
146
 
69
- async deleteRule(name: string): Promise<void> {
70
- return this.rules.delete(name)
147
+ async reload(): Promise<ReloadResult> {
148
+ const res = await this.client.get<{ data: ReloadResult }>(
149
+ `${this.rulesPath}/reload`,
150
+ )
151
+ return res.data!.data
71
152
  }
72
153
 
154
+ async upload(_props: { file: File }): Promise<Document<OperationPolicy> | undefined> {
155
+ throw new Error("Upload not supported for policies")
156
+ }
73
157
 
158
+ async subscribe(_scope: string, _callback: (event: StoreEvent) => void): Promise<() => void> {
159
+ throw new Error("Subscription not supported for policies")
160
+ }
161
+
162
+ async notify(_event: StoreEvent): Promise<void> {
163
+ throw new Error("Notify not supported for policies")
164
+ }
165
+
166
+ stream(_options: Record<string, unknown>, _onStreamChange: () => void): {
167
+ stream: () => AsyncIterable<Document<OperationPolicy>>;
168
+ cancel: () => void;
169
+ status: () => "active" | "cancelled" | "completed";
170
+ } {
171
+ throw new Error("Stream not supported for policies")
172
+ }
173
+
174
+ page(_options?: Record<string, unknown>): PagedData<OperationPolicy> {
175
+ return this.pager
176
+ }
74
177
  }
@@ -1,4 +1,4 @@
1
- export interface PolicyOperation {
1
+ export interface OperationPolicy {
2
2
  name: string
3
3
  ruleKey: string
4
4
  description?: string
@@ -11,7 +11,7 @@ export interface UpsertOperationRequest {
11
11
  intentType?: "COMMAND" | "QUERY"
12
12
  }
13
13
 
14
- export interface PolicyRule {
14
+ export interface IamRule {
15
15
  name: string
16
16
  ruleType?: string
17
17
  expression?: string
package/utils/pager.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { QueryDSL, QueryFilter, SortConfiguration, createMatcher, createSorter } from '@asaidimu/query'; //
1
+ import { createMatcher, createSorter } from '@asaidimu/query'; //
2
+ import type { QueryDSL, QueryFilter, SortConfiguration } from '@asaidimu/query'; //
2
3
  import { DELETE_SYMBOL, ReactiveDataStore } from "@asaidimu/utils-store"; //
3
- import { Page, PagedData } from '../core/types';
4
+ import type { Page, PagedData, PagerRefreshOptions } from '../core/types';
4
5
 
5
6
  export interface FetchResult<T extends Record<string, any>> {
6
7
  data: T[];
@@ -168,6 +169,7 @@ export function createArrayPager<T extends Record<string, any>, F = any>(
168
169
  executeDataPipeline(true);
169
170
 
170
171
  return {
172
+ id: () => CACHE_KEY,
171
173
  page: () => selector.get() ?? sentinel, //
172
174
 
173
175
  navigate: async (page: number) => {
@@ -195,15 +197,17 @@ export function createArrayPager<T extends Record<string, any>, F = any>(
195
197
  await executeDataPipeline(true);
196
198
  },
197
199
 
198
- refresh: async (delay: number = 600) => { //
200
+ refresh: async (opts?: PagerRefreshOptions) => { //
199
201
  const previousState = selector.get() ?? sentinel;
200
202
  await store.set({
201
203
  [CACHE_KEY]: { ...previousState, loading: true } //
202
204
  });
203
205
 
206
+ const delay = opts?.delay ?? 600;
207
+
204
208
  await Promise.all([
205
209
  new Promise((resolve) => setTimeout(resolve, delay)), //
206
- executeDataPipeline(false) // Handle full resolution pipeline passing down false to bypass flashing double mutations
210
+ executeDataPipeline(false) // Handle full resolution pipeline passing down false to avoid flashing double mutations
207
211
  ]);
208
212
 
209
213
  const finishedState = selector.get() ?? sentinel;
@@ -1,147 +0,0 @@
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
- }
@@ -1,38 +0,0 @@
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
- }