identifier builder

This commit is contained in:
PhilReact 2025-03-18 19:09:21 +02:00
parent 4297756240
commit e1204fd95a
7 changed files with 179 additions and 18 deletions

14
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.6", "version": "1.0.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "qapp-core", "name": "qapp-core",
"version": "1.0.6", "version": "1.0.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
@ -18,6 +18,7 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"short-unique-id": "^5.2.0",
"zustand": "^4.3.2" "zustand": "^4.3.2"
}, },
"devDependencies": { "devDependencies": {
@ -2285,6 +2286,15 @@
"node": ">=8" "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": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",

View File

@ -29,6 +29,7 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"short-unique-id": "^5.2.0",
"zustand": "^4.3.2" "zustand": "^4.3.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,12 +2,16 @@ import React, { createContext, useContext, useMemo } from "react";
import { useAuth, UseAuthProps } from "../hooks/useAuth"; import { useAuth, UseAuthProps } from "../hooks/useAuth";
import { useResources } from "../hooks/useResources"; import { useResources } from "../hooks/useResources";
import { useAppInfo } from "../hooks/useAppInfo"; import { useAppInfo } from "../hooks/useAppInfo";
import { IdentifierBuilder } from "../utils/encryption";
import { useIdentifiers } from "../hooks/useIdentifiers";
// ✅ Define Global Context Type // ✅ Define Global Context Type
interface GlobalContextType { interface GlobalContextType {
auth: ReturnType<typeof useAuth>; auth: ReturnType<typeof useAuth>;
resources: ReturnType<typeof useResources>; resources: ReturnType<typeof useResources>;
appInfo: ReturnType<typeof useAppInfo>;
identifierOperations: ReturnType<typeof useIdentifiers>
} }
// ✅ Define Config Type for Hook Options // ✅ Define Config Type for Hook Options
@ -19,21 +23,22 @@ interface GlobalProviderProps {
appName: string; appName: string;
publicSalt: string publicSalt: string
}; };
identifierBuilder?: IdentifierBuilder
} }
// ✅ Create Context with Proper Type // ✅ Create Context with Proper Type
const GlobalContext = createContext<GlobalContextType | null>(null); const GlobalContext = createContext<GlobalContextType | null>(null);
// 🔹 Global Provider (Handles Multiple Hooks) // 🔹 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 // ✅ Call hooks and pass in options dynamically
const auth = useAuth(config?.auth || {}); const auth = useAuth(config?.auth || {});
const appInfo = useAppInfo(config?.appName, config?.publicSalt) const appInfo = useAppInfo(config?.appName, config?.publicSalt)
const resources = useResources() const resources = useResources()
const identifierOperations = useIdentifiers(identifierBuilder, config?.publicSalt)
// ✅ Merge all hooks into a single `contextValue` // ✅ 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 ( return (
<GlobalContext.Provider value={contextValue}> <GlobalContext.Provider value={contextValue}>
{children} {children}

View File

@ -6,10 +6,8 @@ import { EnumCollisionStrength, hashWord } from "../utils/encryption";
export const useAppInfo = (appName?: string, publicSalt?: string) => { export const useAppInfo = (appName?: string, publicSalt?: string) => {
const setAppState = useAppStore().setAppState const setAppState = useAppStore().setAppState
const appNameHashed = useMemo(()=> { const appNameHashed = useAppStore().appNameHashed
if(!appName) return ""
}, [appName])
const handleAppInfoSetup = useCallback(async (name: string, salt: string)=> { const handleAppInfoSetup = useCallback(async (name: string, salt: string)=> {
const appNameHashed = await hashWord(name, EnumCollisionStrength.LOW, salt) const appNameHashed = await hashWord(name, EnumCollisionStrength.LOW, salt)

View 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
};
};

View File

@ -1,12 +1,14 @@
import { create } from "zustand"; import { create } from "zustand";
import { IdentifierBuilder } from "../utils/encryption";
interface AppState { interface AppState {
appName: string | null; appName: string | null;
publicSalt: string | null; publicSalt: string | null;
appNameHashed: string | null; appNameHashed: string | null;
identifierBuilder?: IdentifierBuilder | null
// Methods // Methods
setAppState: (appState: { appName: string; publicSalt: string; appNameHashed: string }) => void; setAppState: (appState: { appName: string; publicSalt: string; appNameHashed: string }) => void;
setIdentifierBuilder: (builder: IdentifierBuilder) => void;
} }
// ✅ Typed Zustand Store // ✅ Typed Zustand Store
@ -14,8 +16,10 @@ export const useAppStore = create<AppState>((set) => ({
appName: null, appName: null,
publicSalt: null, publicSalt: null,
appNameHashed: null, appNameHashed: null,
identifierBuilder: null,
// Methods // Methods
setAppState: (appState) => 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 })
})); }));

View File

@ -1,15 +1,117 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import ShortUniqueId from "short-unique-id";
export enum EnumCollisionStrength { export enum EnumCollisionStrength {
LOW = 8, LOW = 8,
MEDIUM = 11, MEDIUM = 11,
HIGH = 14 HIGH = 14,
PARENT_REF = 14,
ENTITY_LABEL= 6
} }
export async function hashWord(word: string, collisionStrength: number, publicSalt: string) { export async function hashWord(word: string, collisionStrength: number, publicSalt: string) {
const saltedWord = publicSalt + word; // Use public salt directly const saltedWord = publicSalt + word; // Use public salt directly
const encoded = new TextEncoder().encode(saltedWord); const encoded = new TextEncoder().encode(saltedWord);
const hashBuffer = await crypto.subtle.digest("SHA-256", encoded); const hashBuffer = await crypto.subtle.digest("SHA-256", encoded);
return Buffer.from(hashBuffer).toString("base64").slice(0, collisionStrength); // 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}`;
} }