mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-15 01:41:21 +00:00
identifier builder
This commit is contained in:
parent
4297756240
commit
e1204fd95a
14
package-lock.json
generated
14
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
@ -18,6 +18,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"react": "^19.0.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"zustand": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -2285,6 +2286,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/short-unique-id": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.2.0.tgz",
|
||||
"integrity": "sha512-cMGfwNyfDZ/nzJ2k2M+ClthBIh//GlZl1JEf47Uoa9XR11bz8Pa2T2wQO4bVrRdH48LrIDWJahQziKo3MjhsWg==",
|
||||
"bin": {
|
||||
"short-unique-id": "bin/short-unique-id",
|
||||
"suid": "bin/short-unique-id"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
|
@ -29,6 +29,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"react": "^19.0.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"zustand": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -2,12 +2,16 @@ import React, { createContext, useContext, useMemo } from "react";
|
||||
import { useAuth, UseAuthProps } from "../hooks/useAuth";
|
||||
import { useResources } from "../hooks/useResources";
|
||||
import { useAppInfo } from "../hooks/useAppInfo";
|
||||
import { IdentifierBuilder } from "../utils/encryption";
|
||||
import { useIdentifiers } from "../hooks/useIdentifiers";
|
||||
|
||||
|
||||
// ✅ Define Global Context Type
|
||||
interface GlobalContextType {
|
||||
auth: ReturnType<typeof useAuth>;
|
||||
resources: ReturnType<typeof useResources>;
|
||||
appInfo: ReturnType<typeof useAppInfo>;
|
||||
identifierOperations: ReturnType<typeof useIdentifiers>
|
||||
}
|
||||
|
||||
// ✅ Define Config Type for Hook Options
|
||||
@ -19,21 +23,22 @@ interface GlobalProviderProps {
|
||||
appName: string;
|
||||
publicSalt: string
|
||||
};
|
||||
identifierBuilder?: IdentifierBuilder
|
||||
}
|
||||
|
||||
// ✅ Create Context with Proper Type
|
||||
const GlobalContext = createContext<GlobalContextType | null>(null);
|
||||
|
||||
// 🔹 Global Provider (Handles Multiple Hooks)
|
||||
export const GlobalProvider = ({ children, config }: GlobalProviderProps) => {
|
||||
export const GlobalProvider = ({ children, config, identifierBuilder }: GlobalProviderProps) => {
|
||||
// ✅ Call hooks and pass in options dynamically
|
||||
const auth = useAuth(config?.auth || {});
|
||||
const appInfo = useAppInfo(config?.appName, config?.publicSalt)
|
||||
const resources = useResources()
|
||||
const identifierOperations = useIdentifiers(identifierBuilder, config?.publicSalt)
|
||||
|
||||
// ✅ Merge all hooks into a single `contextValue`
|
||||
const contextValue = useMemo(() => ({ auth, resources, appInfo }), [auth, resources, appInfo]);
|
||||
|
||||
const contextValue = useMemo(() => ({ auth, resources, appInfo, identifierOperations }), [auth, resources, appInfo, identifierOperations]);
|
||||
return (
|
||||
<GlobalContext.Provider value={contextValue}>
|
||||
{children}
|
||||
|
@ -6,10 +6,8 @@ import { EnumCollisionStrength, hashWord } from "../utils/encryption";
|
||||
|
||||
export const useAppInfo = (appName?: string, publicSalt?: string) => {
|
||||
const setAppState = useAppStore().setAppState
|
||||
const appNameHashed = useMemo(()=> {
|
||||
if(!appName) return ""
|
||||
|
||||
}, [appName])
|
||||
const appNameHashed = useAppStore().appNameHashed
|
||||
|
||||
|
||||
const handleAppInfoSetup = useCallback(async (name: string, salt: string)=> {
|
||||
const appNameHashed = await hashWord(name, EnumCollisionStrength.LOW, salt)
|
||||
|
41
src/hooks/useIdentifiers.tsx
Normal file
41
src/hooks/useIdentifiers.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useAuthStore } from "../state/auth";
|
||||
import { useAppStore } from "../state/app";
|
||||
import { buildIdentifier, buildSearchPrefix, IdentifierBuilder } from "../utils/encryption";
|
||||
|
||||
|
||||
export const useIdentifiers = (builder?: IdentifierBuilder, publicSalt?: string) => {
|
||||
const setIdentifierBuilder = useAppStore().setIdentifierBuilder
|
||||
const identifierBuilder = useAppStore().identifierBuilder
|
||||
const appName = useAppStore().appName
|
||||
|
||||
const stringifiedBuilder = useMemo(()=> {
|
||||
return JSON.stringify(builder)
|
||||
}, [builder])
|
||||
|
||||
const buildIdentifierFunc = useCallback(( entityType: string,
|
||||
parentId: string | null)=> {
|
||||
if(!appName || !publicSalt || !identifierBuilder) return null
|
||||
return buildIdentifier(appName, publicSalt, entityType, parentId, identifierBuilder)
|
||||
}, [appName, publicSalt, identifierBuilder])
|
||||
|
||||
const buildSearchPrefixFunc = useCallback(( entityType: string,
|
||||
parentId: string | null)=> {
|
||||
if(!appName || !publicSalt || !identifierBuilder) return null
|
||||
return buildSearchPrefix(appName, publicSalt, entityType, parentId, identifierBuilder)
|
||||
}, [appName, publicSalt, identifierBuilder])
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
if(stringifiedBuilder){
|
||||
setIdentifierBuilder(JSON.parse(stringifiedBuilder))
|
||||
}
|
||||
}, [stringifiedBuilder])
|
||||
return {
|
||||
identifierBuilder,
|
||||
buildIdentifier: buildIdentifierFunc,
|
||||
buildSearchPrefix: buildSearchPrefixFunc
|
||||
};
|
||||
};
|
@ -1,12 +1,14 @@
|
||||
import { create } from "zustand";
|
||||
import { IdentifierBuilder } from "../utils/encryption";
|
||||
|
||||
interface AppState {
|
||||
appName: string | null;
|
||||
publicSalt: string | null;
|
||||
appNameHashed: string | null;
|
||||
|
||||
identifierBuilder?: IdentifierBuilder | null
|
||||
// Methods
|
||||
setAppState: (appState: { appName: string; publicSalt: string; appNameHashed: string }) => void;
|
||||
setIdentifierBuilder: (builder: IdentifierBuilder) => void;
|
||||
}
|
||||
|
||||
// ✅ Typed Zustand Store
|
||||
@ -14,8 +16,10 @@ export const useAppStore = create<AppState>((set) => ({
|
||||
appName: null,
|
||||
publicSalt: null,
|
||||
appNameHashed: null,
|
||||
|
||||
identifierBuilder: null,
|
||||
// Methods
|
||||
setAppState: (appState) =>
|
||||
set({ appName: appState.appName, publicSalt: appState.publicSalt, appNameHashed: appState.appNameHashed })
|
||||
set({ appName: appState.appName, publicSalt: appState.publicSalt, appNameHashed: appState.appNameHashed }),
|
||||
setIdentifierBuilder: (identifierBuilder) =>
|
||||
set({ identifierBuilder })
|
||||
}));
|
||||
|
@ -1,15 +1,117 @@
|
||||
import { Buffer } from "buffer";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
|
||||
export enum EnumCollisionStrength {
|
||||
LOW = 8,
|
||||
MEDIUM = 11,
|
||||
HIGH = 14
|
||||
HIGH = 14,
|
||||
PARENT_REF = 14,
|
||||
ENTITY_LABEL= 6
|
||||
}
|
||||
|
||||
export async function hashWord(word: string, collisionStrength: number, publicSalt: string) {
|
||||
const saltedWord = publicSalt + word; // Use public salt directly
|
||||
const encoded = new TextEncoder().encode(saltedWord);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
|
||||
|
||||
return Buffer.from(hashBuffer).toString("base64").slice(0, collisionStrength);
|
||||
export async function hashWord(word: string, collisionStrength: number, publicSalt: string) {
|
||||
const saltedWord = publicSalt + word; // Use public salt directly
|
||||
const encoded = new TextEncoder().encode(saltedWord);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
|
||||
|
||||
// Convert to base64 and make it URL-safe
|
||||
return Buffer.from(hashBuffer)
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-") // Replace '+' with '-'
|
||||
.replace(/\//g, "_") // Replace '/' with '_'
|
||||
.replace(/=+$/, "") // Remove trailing '='
|
||||
.slice(0, collisionStrength);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const uid = new ShortUniqueId({ length: 10, dictionary: "alphanum" });
|
||||
|
||||
interface EntityConfig {
|
||||
children?: Record<string, EntityConfig>;
|
||||
}
|
||||
|
||||
export type IdentifierBuilder = {
|
||||
[key: string]: {
|
||||
children?: IdentifierBuilder;
|
||||
};
|
||||
};
|
||||
|
||||
// Recursive function to traverse identifierBuilder
|
||||
function findEntityConfig(identifierBuilder: IdentifierBuilder, path: string[]): EntityConfig {
|
||||
let current: EntityConfig | undefined = { children: identifierBuilder }; // ✅ Wrap it inside `{ children }` so it behaves like other levels
|
||||
|
||||
|
||||
for (const key of path) {
|
||||
if (!current.children || !current.children[key]) {
|
||||
throw new Error(`Entity '${key}' is not defined in identifierBuilder`);
|
||||
}
|
||||
current = current.children[key];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
|
||||
// Function to generate a prefix for searching
|
||||
export async function buildSearchPrefix(
|
||||
appName: string,
|
||||
publicSalt: string,
|
||||
entityType: string,
|
||||
parentId: string | null,
|
||||
identifierBuilder: IdentifierBuilder
|
||||
): Promise<string> {
|
||||
// Hash app name (11 chars)
|
||||
const appHash: string = await hashWord(appName, EnumCollisionStrength.HIGH, publicSalt);
|
||||
|
||||
// Hash entity type (4 chars)
|
||||
const entityPrefix: string = await hashWord(entityType, EnumCollisionStrength.ENTITY_LABEL, publicSalt);
|
||||
|
||||
// ✅ Detect if this entity is actually a root entity
|
||||
const isRootEntity = !!identifierBuilder[entityType];
|
||||
|
||||
// Determine parent reference
|
||||
let parentRef = "";
|
||||
if (isRootEntity && parentId === null) {
|
||||
parentRef = "00000000000000"; // ✅ Only for true root entities
|
||||
} else if (parentId) {
|
||||
parentRef = await hashWord(parentId, EnumCollisionStrength.PARENT_REF, publicSalt);
|
||||
}
|
||||
|
||||
// ✅ If there's no parentRef, return without it
|
||||
return parentRef
|
||||
? `${appHash}-${entityPrefix}-${parentRef}-` // ✅ Normal case with a parent
|
||||
: `${appHash}-${entityPrefix}-`; // ✅ Global search for entity type
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Function to generate IDs dynamically with `publicSalt`
|
||||
export async function buildIdentifier(
|
||||
appName: string,
|
||||
publicSalt: string,
|
||||
entityType: string, // ✅ Now takes only the entity type
|
||||
parentId: string | null,
|
||||
identifierBuilder: IdentifierBuilder
|
||||
): Promise<string> {
|
||||
console.log("Entity Type:", entityType); // Debugging
|
||||
console.log("Parent ID:", parentId); // Debugging
|
||||
|
||||
// Hash app name (11 chars)
|
||||
const appHash: string = await hashWord(appName, EnumCollisionStrength.HIGH, publicSalt);
|
||||
|
||||
// Hash entity type (4 chars)
|
||||
const entityPrefix: string = await hashWord(entityType, EnumCollisionStrength.ENTITY_LABEL, publicSalt);
|
||||
|
||||
// Generate a unique identifier for this entity
|
||||
const entityUid = uid.rnd();
|
||||
|
||||
// Determine parent reference
|
||||
let parentRef = "00000000000000"; // Default for feeds
|
||||
if (parentId) {
|
||||
parentRef = await hashWord(parentId, EnumCollisionStrength.PARENT_REF, publicSalt);
|
||||
}
|
||||
|
||||
return `${appHash}-${entityPrefix}-${parentRef}-${entityUid}`;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user