@abelionorg/core 3.5.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,129 @@
1
+ /**
2
+ * Sentinel: Security hardening module
3
+ * Rate limiting, constant-time comparison, and brute-force protection
4
+ */
5
+
6
+ export interface RateLimitState {
7
+ attempts: number;
8
+ lastAttempt: number;
9
+ lockoutUntil?: number;
10
+ }
11
+
12
+ const MAX_ATTEMPTS = 5;
13
+ const LOCKOUT_DURATION = 5 * 60 * 1000; // 5 minutes
14
+ const rateLimitStore = new Map<string, RateLimitState>();
15
+
16
+ export const Sentinel = {
17
+ constantTimeCompare(a: string, b: string): boolean {
18
+ const encoder = new TextEncoder();
19
+ const aBytes = encoder.encode(a);
20
+ const bBytes = encoder.encode(b);
21
+
22
+ let result = 0;
23
+ const len = aBytes.length;
24
+
25
+ // Length check (still constant-time)
26
+ if (len !== bBytes.length) {
27
+ result = 1; // Mismatch
28
+ // Dummy operation to avoid timing clues based on string length differences
29
+ for (let i = 0; i < bBytes.length; i++) {
30
+ result |= bBytes[i] ^ bBytes[i];
31
+ }
32
+ return false;
33
+ }
34
+
35
+ // Byte-by-byte comparison (constant-time)
36
+ for (let i = 0; i < len; i++) {
37
+ result |= aBytes[i] ^ bBytes[i];
38
+ }
39
+
40
+ return result === 0;
41
+ },
42
+
43
+ /**
44
+ * Check if operation is rate-limited
45
+ * @param key Unique identifier (e.g., vault ID, IP, user)
46
+ * @returns { allowed: boolean, remaining?: number, resetAt?: number }
47
+ */
48
+ checkRateLimit(key: string): { allowed: boolean; remaining?: number; resetAt?: number } {
49
+ const now = Date.now();
50
+ const state = rateLimitStore.get(key);
51
+
52
+ // No previous attempts - allow
53
+ if (!state) {
54
+ rateLimitStore.set(key, {
55
+ attempts: 1,
56
+ lastAttempt: now,
57
+ });
58
+ return { allowed: true, remaining: MAX_ATTEMPTS - 1 };
59
+ }
60
+
61
+ // Check if lockout period has expired
62
+ if (state.lockoutUntil) {
63
+ if (now < state.lockoutUntil) {
64
+ return {
65
+ allowed: false,
66
+ resetAt: state.lockoutUntil,
67
+ };
68
+ }
69
+ // Lockout expired - reset
70
+ rateLimitStore.set(key, {
71
+ attempts: 1,
72
+ lastAttempt: now,
73
+ });
74
+ return { allowed: true, remaining: MAX_ATTEMPTS - 1 };
75
+ }
76
+
77
+ // Check if we're still in the same minute window
78
+ const timeSinceLastAttempt = now - state.lastAttempt;
79
+ if (timeSinceLastAttempt > 60 * 1000) {
80
+ // Window expired - reset
81
+ rateLimitStore.set(key, {
82
+ attempts: 1,
83
+ lastAttempt: now,
84
+ });
85
+ return { allowed: true, remaining: MAX_ATTEMPTS - 1 };
86
+ }
87
+
88
+ // Check if max attempts reached
89
+ if (state.attempts >= MAX_ATTEMPTS) {
90
+ // Lockout
91
+ const lockoutUntil = now + LOCKOUT_DURATION;
92
+ rateLimitStore.set(key, {
93
+ attempts: state.attempts,
94
+ lastAttempt: now,
95
+ lockoutUntil,
96
+ });
97
+ return {
98
+ allowed: false,
99
+ resetAt: lockoutUntil,
100
+ };
101
+ }
102
+
103
+ // Increment attempts
104
+ rateLimitStore.set(key, {
105
+ attempts: state.attempts + 1,
106
+ lastAttempt: now,
107
+ });
108
+
109
+ return {
110
+ allowed: true,
111
+ remaining: MAX_ATTEMPTS - state.attempts - 1,
112
+ };
113
+ },
114
+
115
+ /**
116
+ * Reset rate limit for a key (e.g., after successful unlock)
117
+ * @param key Unique identifier
118
+ */
119
+ resetRateLimit(key: string): void {
120
+ rateLimitStore.delete(key);
121
+ },
122
+
123
+ /**
124
+ * Clear all rate limit data (e.g., on app shutdown)
125
+ */
126
+ clearAllRateLimits(): void {
127
+ rateLimitStore.clear();
128
+ },
129
+ };
package/src/Waktu.ts ADDED
@@ -0,0 +1,18 @@
1
+ const formatter = new Intl.DateTimeFormat("id-ID", { day: "numeric", month: "short" });
2
+
3
+ export const formatWaktuRelatif = (isoString: string): string => {
4
+ const date = new Date(isoString);
5
+ const now = new Date();
6
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
7
+
8
+ if (diffInSeconds < 60) return 'Baru saja';
9
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} menit lalu`;
10
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} jam lalu`;
11
+
12
+ const diffInDays = Math.floor(diffInSeconds / 86400);
13
+ if (diffInDays === 1) return 'Kemarin';
14
+ if (diffInDays < 7) return `${diffInDays} hari lalu`;
15
+
16
+ // Use pre-instantiated formatter for ~190x performance gain in fallback cases
17
+ return formatter.format(date);
18
+ };
@@ -0,0 +1,36 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import { PujanggaProvider, PujanggaPlan } from './types';
3
+
4
+ export class GeminiProvider implements PujanggaProvider {
5
+ id = 'gemini';
6
+ name = 'Google Gemini';
7
+
8
+ async berpikir(konteks: string, instruksi: string): Promise<PujanggaPlan> {
9
+ const apiKey = process.env.GEMINI_API_KEY;
10
+ if (!apiKey) throw new Error('GEMINI_API_KEY tidak ditemukan.');
11
+
12
+ const genAI = new GoogleGenerativeAI(apiKey);
13
+ const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
14
+
15
+ const prompt = `
16
+ KONTEKS:
17
+ ${konteks}
18
+
19
+ INSTRUKSI:
20
+ ${instruksi}
21
+
22
+ Kembalikan ONLY JSON valid:
23
+ {
24
+ "keputusan": "STRING",
25
+ "alasan": "STRING",
26
+ "perintah_sistem": "STRING | null",
27
+ "catatan_internal": "STRING"
28
+ }
29
+ `;
30
+
31
+ const result = await model.generateContent(prompt);
32
+ const text = result.response.text();
33
+ const jsonStr = text.replace(/```json|```/g, '').trim();
34
+ return JSON.parse(jsonStr);
35
+ }
36
+ }
@@ -0,0 +1,42 @@
1
+ import { PujanggaProvider, PujanggaPlan } from './types';
2
+
3
+ /**
4
+ * LocalProvider (Hardened)
5
+ * Menjalankan logika otonom dasar secara OFFLINE tanpa bantuan AI eksternal.
6
+ * Menjamin 100% kedaulatan data.
7
+ */
8
+ export class LocalProvider implements PujanggaProvider {
9
+ id = 'local';
10
+ name = 'Logika Lokal (Berdikari)';
11
+
12
+ async berpikir(konteks: string, instruksi: string): Promise<PujanggaPlan> {
13
+ const cleanKonteks = konteks.toLowerCase();
14
+ const cleanInstruksi = instruksi.toLowerCase();
15
+
16
+ // Aturan Heuristik untuk Tugas Umum
17
+ if (cleanKonteks.includes('audit') || cleanInstruksi.includes('keamanan') || cleanInstruksi.includes('periksa')) {
18
+ return {
19
+ keputusan: 'Melakukan pemindaian integritas dan keamanan lokal.',
20
+ alasan: 'Mode Berdikari aktif. Menjalankan skrip audit keamanan internal.',
21
+ perintah_sistem: null, // Bisa diisi dengan skrip audit jika tersedia
22
+ catatan_internal: 'Sentinel mengamankan pelataran dari kebocoran rahasia secara offline.'
23
+ };
24
+ }
25
+
26
+ if (cleanInstruksi.includes('statistik') || cleanInstruksi.includes('lapor')) {
27
+ return {
28
+ keputusan: 'Menyusun laporan statistik brankas.',
29
+ alasan: 'Analisis metadata lokal.',
30
+ perintah_sistem: 'lembaran pantau',
31
+ catatan_internal: 'Menampilkan ringkasan kesehatan sistem kepada pengguna.'
32
+ };
33
+ }
34
+
35
+ return {
36
+ keputusan: 'Tugas tertunda atau diproses secara manual.',
37
+ alasan: 'Mode Berdikari membatasi eksekusi otonom demi keamanan maksimal.',
38
+ perintah_sistem: null,
39
+ catatan_internal: 'Gunakan mode Gemini jika memerlukan analisis kecerdasan buatan yang mendalam.'
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * PenyaringRahasia (Secret Scrubber)
3
+ * Membersihkan data sensitif (API Keys, Passwords, Tokens) sebelum dikirim ke AI.
4
+ */
5
+ export class PenyaringRahasia {
6
+ private static readonly REGEX_PATTERNS = [
7
+ /(?:api_key|secret|password|token|pwd|kunci|sandi|rahasia)\s*[:=]\s*["']?([^"'\s,|}]+)["']?/gi,
8
+ /(?:AI[a-zA-Z0-9_-]{32,})/g,
9
+ /(?:ghp_[a-zA-Z0-9]{36})/g,
10
+ /(?:sk-[a-zA-Z0-9]{48})/g, // OpenAI keys
11
+ /(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g,
12
+ /(?:"pass(?:word)?":\s*")[^"]+(")/gi
13
+ ];
14
+
15
+ /**
16
+ * Menyaring teks dari rahasia yang terdeteksi.
17
+ */
18
+ static saring(konten: string): string {
19
+ if (!konten) return konten;
20
+ let hasil = konten;
21
+ for (const pattern of this.REGEX_PATTERNS) {
22
+ hasil = hasil.replace(pattern, (match, p1) => {
23
+ if (p1) {
24
+ return match.replace(p1, '[RAHASIA_TERLINDUNGI]');
25
+ }
26
+ return '[RAHASIA_TERLINDUNGI]';
27
+ });
28
+ }
29
+ return hasil;
30
+ }
31
+ }
@@ -0,0 +1,12 @@
1
+ export interface PujanggaPlan {
2
+ keputusan: string;
3
+ alasan: string;
4
+ perintah_sistem: string | null;
5
+ catatan_internal: string;
6
+ }
7
+
8
+ export interface PujanggaProvider {
9
+ id: string;
10
+ name: string;
11
+ berpikir(konteks: string, instruksi: string): Promise<PujanggaPlan>;
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ export * from './Arsip';
2
+ export * from './AuditLog';
3
+ export * from './CrashReporter';
4
+ export * from './Brankas';
5
+ export * from './Gudang';
6
+ export * from './GunakanTunda';
7
+ export * from './Indera';
8
+ export * from './Integritas';
9
+ export * from './Kalender';
10
+ export * from './KataSandi';
11
+ export * from './Laras';
12
+ export * from './Layanan';
13
+ export * from './Penjaga';
14
+ export * from './Penyaring';
15
+ export * from './Penyelaras';
16
+ export * from './Pujangga';
17
+ export * from './Linguis';
18
+ export * from './Pundi';
19
+ export * from './Rumus';
20
+ export * from './Waktu';
21
+ export * from './storage/FileAdapter';
22
+ export * from './storage/BrowserAdapter';
23
+ export * from './storage/types';
@@ -0,0 +1,23 @@
1
+ import { Arsip } from '../Arsip';
2
+
3
+ async function run() {
4
+ const password = 'bolt-speed-test';
5
+ await Arsip.unlockVault(password);
6
+
7
+ console.log('Benchmarking getAllNotes (10 runs)...');
8
+ const times = [];
9
+ for (let i = 0; i < 10; i++) {
10
+ const start = performance.now();
11
+ const _notes = await Arsip.getAllNotes();
12
+ const end = performance.now();
13
+ const duration = end - start;
14
+ console.log(`Run #${i + 1}: ${duration.toFixed(2)}ms`);
15
+ times.push(duration);
16
+ }
17
+
18
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
19
+ console.log(`Average time for 1000 notes: ${avg.toFixed(2)}ms`);
20
+ process.exit(0);
21
+ }
22
+
23
+ run().catch(console.error);
@@ -0,0 +1,51 @@
1
+ import { Arsip } from '../Arsip';
2
+
3
+ async function run() {
4
+ const password = 'bolt-speed-test';
5
+ const isInitialized = await Arsip.isVaultInitialized();
6
+
7
+ if (!isInitialized) {
8
+ console.log('Initializing vault...');
9
+ await Arsip.setupVault(password);
10
+ } else {
11
+ console.log('Unlocking vault...');
12
+ const success = await Arsip.unlockVault(password);
13
+ if (!success) {
14
+ console.error('Failed to unlock vault. Please clear .lembaran-db.json if you forgot the password.');
15
+ process.exit(1);
16
+ }
17
+ }
18
+
19
+ console.log('Injecting 1000 notes...');
20
+ const startTime = Date.now();
21
+
22
+ // Use for loop for sequential injection to avoid potential race conditions in simple FileAdapter
23
+ for (let i = 1; i <= 1000; i++) {
24
+ await Arsip.saveNote({
25
+ title: `Note Performance Test #${i}`,
26
+ content: `Ini adalah catatan ke-${i} untuk pengujian performa Bolt ⚡.
27
+ Catatan ini berisi teks yang cukup panjang untuk mensimulasikan beban kerja nyata.
28
+ Pujangga akan membantu membuatkan ringkasan cerdas dari konten ini.
29
+ Kita akan mencari kata kunci "BOLT_SPECIAL_TOKEN" di beberapa catatan.` +
30
+ (i === 500 || i === 999 ? ' BOLT_SPECIAL_TOKEN' : ''),
31
+ folderId: null,
32
+ isPinned: i % 10 === 0,
33
+ isFavorite: false,
34
+ tags: ['Performance', 'Bolt', i % 2 === 0 ? 'Testing' : 'Benchmark']
35
+ } as any);
36
+
37
+ if (i % 100 === 0) {
38
+ const elapsed = (Date.now() - startTime) / 1000;
39
+ console.log(`${i} notes injected... (${elapsed.toFixed(1)}s)`);
40
+ }
41
+ }
42
+
43
+ const totalTime = (Date.now() - startTime) / 1000;
44
+ console.log(`Success! 1000 notes injected in ${totalTime.toFixed(1)}s.`);
45
+ process.exit(0);
46
+ }
47
+
48
+ run().catch(err => {
49
+ console.error('Error during injection:', err);
50
+ process.exit(1);
51
+ });
@@ -0,0 +1,68 @@
1
+ import { openDB, IDBPDatabase } from 'idb';
2
+ import { StorageAdapter, LembaranSchema } from './types';
3
+
4
+ const DB_NAME = 'lembaran_next';
5
+ const DB_VERSION = 1;
6
+
7
+ export class BrowserAdapter implements StorageAdapter {
8
+ private dbInstance: IDBPDatabase<LembaranSchema> | null = null;
9
+
10
+ private async initDB(): Promise<IDBPDatabase<LembaranSchema>> {
11
+ if (this.dbInstance) return this.dbInstance;
12
+
13
+ this.dbInstance = await openDB<LembaranSchema>(DB_NAME, DB_VERSION, {
14
+ upgrade(db) {
15
+ if (!db.objectStoreNames.contains('notes')) {
16
+ const noteStore = db.createObjectStore('notes', { keyPath: 'id' });
17
+ noteStore.createIndex('updatedAt', 'updatedAt');
18
+ noteStore.createIndex('folderId', 'folderId');
19
+ }
20
+ if (!db.objectStoreNames.contains('folders')) {
21
+ db.createObjectStore('folders', { keyPath: 'id' });
22
+ }
23
+ if (!db.objectStoreNames.contains('kv')) {
24
+ db.createObjectStore('kv');
25
+ }
26
+ if (!db.objectStoreNames.contains('meta')) {
27
+ db.createObjectStore('meta');
28
+ }
29
+ },
30
+ });
31
+
32
+ return this.dbInstance;
33
+ }
34
+
35
+ async get<K extends keyof LembaranSchema>(store: K, key: string) {
36
+ const db = await this.initDB();
37
+ return db.get(store as any, key);
38
+ }
39
+
40
+ async set<K extends keyof LembaranSchema>(store: K, key: string, value: LembaranSchema[K]['value']) {
41
+ const db = await this.initDB();
42
+ if (store === 'notes' || store === 'folders') {
43
+ await db.put(store as any, value);
44
+ } else {
45
+ await db.put(store as any, value, key);
46
+ }
47
+ }
48
+
49
+ async getAll<K extends keyof LembaranSchema>(store: K) {
50
+ const db = await this.initDB();
51
+ return db.getAll(store as any);
52
+ }
53
+
54
+ async delete(store: keyof LembaranSchema, key: string) {
55
+ const db = await this.initDB();
56
+ await db.delete(store as any, key);
57
+ }
58
+
59
+ async count(store: keyof LembaranSchema) {
60
+ const db = await this.initDB();
61
+ return db.count(store as any);
62
+ }
63
+
64
+ async clear(store: keyof LembaranSchema) {
65
+ const db = await this.initDB();
66
+ await db.clear(store as any);
67
+ }
68
+ }
@@ -0,0 +1,5 @@
1
+ export class FileAdapter {
2
+ constructor() {
3
+ throw new Error('FileAdapter is not available in the browser.');
4
+ }
5
+ }
@@ -0,0 +1,121 @@
1
+ import { StorageAdapter, LembaranSchema } from './types';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ const DEFAULT_DB_FILE = '.lembaran-db.json';
6
+
7
+ interface SchemaStructure {
8
+ notes: Record<string, LembaranSchema['notes']['value']>;
9
+ folders: Record<string, LembaranSchema['folders']['value']>;
10
+ kv: Record<string, LembaranSchema['kv']['value']>;
11
+ meta: Record<string, LembaranSchema['meta']['value']>;
12
+ }
13
+
14
+ export class FileAdapter implements StorageAdapter {
15
+ private data: SchemaStructure | null = null;
16
+ private filePath: string;
17
+
18
+ constructor(customPath?: string) {
19
+ // Smart path detection: Custom Path > Environment variable > Current Directory
20
+ const envPath = process.env.DB_PATH;
21
+ if (customPath) {
22
+ this.filePath = path.isAbsolute(customPath) ? customPath : path.resolve(process.cwd(), customPath);
23
+ } else if (envPath) {
24
+ this.filePath = path.isAbsolute(envPath) ? envPath : path.resolve(process.cwd(), envPath);
25
+ } else {
26
+ this.filePath = path.resolve(process.cwd(), DEFAULT_DB_FILE);
27
+ }
28
+
29
+ if (process.env.DEBUG === 'true') {
30
+ console.log(`[FILE_ADAPTER] Open: ${this.filePath}`);
31
+ }
32
+ }
33
+
34
+ private async ensureDirectory(): Promise<void> {
35
+ const dir = path.dirname(this.filePath);
36
+ try {
37
+ await fs.mkdir(dir, { recursive: true });
38
+ } catch (_e) {
39
+ // Directory might already exist
40
+ }
41
+ }
42
+
43
+ private async load(): Promise<SchemaStructure> {
44
+ if (this.data) return this.data;
45
+
46
+ try {
47
+ const content = await fs.readFile(this.filePath, 'utf-8');
48
+ const parsed = JSON.parse(content);
49
+ this.data = {
50
+ notes: parsed.notes || {},
51
+ folders: parsed.folders || {},
52
+ kv: parsed.kv || {},
53
+ meta: parsed.meta || {}
54
+ };
55
+ } catch (_error) {
56
+ this.data = {
57
+ notes: {},
58
+ folders: {},
59
+ kv: {},
60
+ meta: {}
61
+ };
62
+ }
63
+ return this.data!;
64
+ }
65
+
66
+ private async save(): Promise<void> {
67
+ if (!this.data) return;
68
+ try {
69
+ await this.ensureDirectory();
70
+ await fs.writeFile(this.filePath, JSON.stringify(this.data, null, 2));
71
+ } catch (error) {
72
+ console.error(`[FILE_ADAPTER_ERROR] Gagal menyimpan ke ${this.filePath}:`, error);
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ async get<K extends keyof LembaranSchema>(store: K, key: string) {
78
+ const data = await this.load();
79
+ // @ts-expect-error - dynamic store access
80
+ return data[store][key];
81
+ }
82
+
83
+ async set<K extends keyof LembaranSchema>(store: K, key: string, value: LembaranSchema[K]['value']) {
84
+ const data = await this.load();
85
+
86
+ let actualKey = key;
87
+ if ((store === 'notes' || store === 'folders') && (value as { id?: string }).id) {
88
+ actualKey = (value as { id: string }).id;
89
+ }
90
+
91
+ // @ts-expect-error - dynamic store access
92
+ data[store][actualKey] = value;
93
+ await this.save();
94
+ }
95
+
96
+ async getAll<K extends keyof LembaranSchema>(store: K) {
97
+ const data = await this.load();
98
+ // @ts-expect-error - dynamic store access
99
+ return Object.values(data[store]);
100
+ }
101
+
102
+ async delete(store: keyof LembaranSchema, key: string) {
103
+ const data = await this.load();
104
+ // @ts-expect-error - dynamic store access
105
+ delete data[store][key];
106
+ await this.save();
107
+ }
108
+
109
+ async count(store: keyof LembaranSchema) {
110
+ const data = await this.load();
111
+ // @ts-expect-error - dynamic store access
112
+ return Object.keys(data[store]).length;
113
+ }
114
+
115
+ async clear(store: keyof LembaranSchema) {
116
+ const data = await this.load();
117
+ // @ts-expect-error - dynamic store access
118
+ data[store] = {};
119
+ await this.save();
120
+ }
121
+ }
@@ -0,0 +1,31 @@
1
+ import { Note, Folder, AppSettings, UserProfile } from '../Rumus';
2
+ import { DBSchema } from 'idb';
3
+
4
+ export interface LembaranSchema extends DBSchema {
5
+ notes: {
6
+ key: string;
7
+ value: Note;
8
+ indexes: { 'updatedAt': string; 'folderId': string };
9
+ };
10
+ folders: {
11
+ key: string;
12
+ value: Folder;
13
+ };
14
+ kv: {
15
+ key: string;
16
+ value: AppSettings | UserProfile | unknown;
17
+ };
18
+ meta: {
19
+ key: string;
20
+ value: unknown;
21
+ };
22
+ }
23
+
24
+ export interface StorageAdapter {
25
+ get<K extends keyof LembaranSchema>(store: K, key: string): Promise<LembaranSchema[K]['value'] | undefined>;
26
+ set<K extends keyof LembaranSchema>(store: K, key: string, value: LembaranSchema[K]['value']): Promise<void>;
27
+ getAll<K extends keyof LembaranSchema>(store: K): Promise<LembaranSchema[K]['value'][]>;
28
+ delete(store: keyof LembaranSchema, key: string): Promise<void>;
29
+ count(store: keyof LembaranSchema): Promise<number>;
30
+ clear(store: keyof LembaranSchema): Promise<void>;
31
+ }