@modernman00/shared-js-lib 1.2.90 → 1.6.2
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.
- package/AddressAutocomplete.js +164 -15
- package/AiBillSplitter.js +230 -0
- package/AiEntertainmentPrompt.js +130 -0
- package/AiFlyerStudio.js +215 -0
- package/AiMediaGuard.js +95 -0
- package/AiPrivacyGuard.js +63 -0
- package/AiSemanticSearch.js +107 -0
- package/AiVoiceRecorder.js +196 -0
- package/CanvasPhysicsEngine.js +568 -0
- package/FileUpload.js +182 -1
- package/HyperReality3DEngine.js +404 -0
- package/Loader.js +6 -2
- package/PwaInstall.js +20 -2
- package/README.md +78 -0
- package/SovereignBroadcasterClient.js +325 -0
- package/VisualSupremacyRuntime.js +312 -0
- package/WebAuthnClient.js +346 -1
- package/WebPushClient.js +308 -0
- package/axiosWrapper.js +1 -0
- package/index.js +13 -1
- package/package.json +13 -1
- package/templates/sw.template.js +105 -57
package/README.md
CHANGED
|
@@ -129,6 +129,84 @@ mix.js('resources/js/app.js', 'public/js')
|
|
|
129
129
|
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
### Example: AI Client Utilities (£0 Serverless AI)
|
|
133
|
+
```javascript
|
|
134
|
+
import { AiVoiceRecorder, AiMediaGuard, AiSemanticSearch } from '@modernman00/shared-js-lib';
|
|
135
|
+
|
|
136
|
+
// 1. Voice-to-Text Recording (Whisper via Backend Proxy)
|
|
137
|
+
const recorder = new AiVoiceRecorder({
|
|
138
|
+
uploadUrl: '/api/ai/transcribe',
|
|
139
|
+
onStateChange: (state) => console.log('Recording state:', state),
|
|
140
|
+
onError: (err) => alert(err.message)
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await recorder.start();
|
|
144
|
+
// ... later
|
|
145
|
+
const blob = await recorder.stop();
|
|
146
|
+
const { text } = await recorder.uploadAndTranscribe(blob);
|
|
147
|
+
document.querySelector('#task-input').value = text;
|
|
148
|
+
|
|
149
|
+
// 2. Photo NSFW Guard (Fail-Closed Image Safety Pre-check)
|
|
150
|
+
const file = document.querySelector('input[type="file"]').files[0];
|
|
151
|
+
const validation = AiMediaGuard.validateFile(file, { maxSizeBytes: 10 * 1024 * 1024 });
|
|
152
|
+
if (!validation.valid) {
|
|
153
|
+
alert(validation.error);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const safety = await AiMediaGuard.precheckPhotoSafety(file, '/api/ai/moderate-photo');
|
|
157
|
+
if (safety.flagged) {
|
|
158
|
+
alert('Photo rejected: content contains NSFW imagery.');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Instant Semantic Search (Debounced & Abort-Protected)
|
|
162
|
+
const searchController = new AiSemanticSearch({
|
|
163
|
+
searchUrl: '/api/ai/semantic-search',
|
|
164
|
+
minQueryLength: 3,
|
|
165
|
+
debounceMs: 350,
|
|
166
|
+
onResults: (results) => renderCards(results),
|
|
167
|
+
onLoading: (isLoading) => toggleSpinner(isLoading)
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
document.querySelector('#search-input').addEventListener('input', (e) => {
|
|
171
|
+
searchController.handleInput(e.target.value);
|
|
172
|
+
});
|
|
173
|
+
// 4. Generative Event Flyer Studio (AI Art + Razor-Sharp Canvas Typography)
|
|
174
|
+
import { AiFlyerStudio, AiBillSplitter, AiEntertainmentPrompt } from '@modernman00/shared-js-lib';
|
|
175
|
+
|
|
176
|
+
const studio = new AiFlyerStudio({ width: 800, height: 1200 });
|
|
177
|
+
await studio.setBackgroundImage(aiGeneratedBackgroundDataUri);
|
|
178
|
+
studio.setData({
|
|
179
|
+
title: 'Liam 7th Birthday',
|
|
180
|
+
subtitle: 'Jurassic Dinosaur Adventure',
|
|
181
|
+
date: 'Saturday, 12 Nov',
|
|
182
|
+
time: '3:00 PM',
|
|
183
|
+
location: 'St. James Hall',
|
|
184
|
+
hostName: 'Sarah & Mark'
|
|
185
|
+
});
|
|
186
|
+
studio.render();
|
|
187
|
+
studio.download('liam-birthday-flyer.png');
|
|
188
|
+
|
|
189
|
+
// 5. Multimodal Bill & Bar Tab Splitter (Camera Photo -> Itemized Guest Assignment)
|
|
190
|
+
const splitter = new AiBillSplitter({ defaultTipPercent: 12.5 });
|
|
191
|
+
const scanResult = await splitter.scanReceipt(receiptPhotoFile);
|
|
192
|
+
if (scanResult.success) {
|
|
193
|
+
splitter.addGuest('g1', 'Alice');
|
|
194
|
+
splitter.addGuest('g2', 'Bob');
|
|
195
|
+
splitter.assignItem(0, 'g1'); // Alice pays for Nachos
|
|
196
|
+
splitter.assignItem(0, 'g2'); // Bob shares Nachos
|
|
197
|
+
const breakdown = splitter.calculateShares(10); // per-guest breakdown with tip
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 6. AI Speechwriter & Roast Master
|
|
201
|
+
const speech = await AiEntertainmentPrompt.requestSpeech({
|
|
202
|
+
honoureeName: 'Dave',
|
|
203
|
+
occasion: '30th Birthday',
|
|
204
|
+
anecdotes: ['Fell into pool in Ibiza', 'Sourdough bread fanatic'],
|
|
205
|
+
tone: 'humorous_roast'
|
|
206
|
+
});
|
|
207
|
+
console.log(speech.speechText, speech.toast);
|
|
208
|
+
```
|
|
209
|
+
|
|
132
210
|
Updating the Package
|
|
133
211
|
To update to the latest version, run:
|
|
134
212
|
npm update @modernman00/shared-js-lib
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SovereignBroadcasterClient
|
|
3
|
+
*
|
|
4
|
+
* Engineered by the Advanced Technology Group (ATG) under Olutobi (Deloitte) and Victor (CTO).
|
|
5
|
+
* High-performance, zero-npm, drop-in replacement for Pusher JS.
|
|
6
|
+
* Connects directly to SovereignBroadcaster PHP SSE endpoints with auto-reconnect,
|
|
7
|
+
* HMAC channel authentication, and presence tracking.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class SovereignChannel {
|
|
11
|
+
constructor(name, broadcaster) {
|
|
12
|
+
this.name = name;
|
|
13
|
+
this.broadcaster = broadcaster;
|
|
14
|
+
this.callbacks = new Map();
|
|
15
|
+
this.members = new Map();
|
|
16
|
+
this.subscribed = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
bind(event, callback) {
|
|
20
|
+
if (!this.callbacks.has(event)) {
|
|
21
|
+
this.callbacks.set(event, []);
|
|
22
|
+
}
|
|
23
|
+
this.callbacks.get(event).push(callback);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
unbind(event, callback) {
|
|
28
|
+
if (!this.callbacks.has(event)) return this;
|
|
29
|
+
if (!callback) {
|
|
30
|
+
this.callbacks.delete(event);
|
|
31
|
+
} else {
|
|
32
|
+
const filtered = this.callbacks.get(event).filter(cb => cb !== callback);
|
|
33
|
+
this.callbacks.set(event, filtered);
|
|
34
|
+
}
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
emit(event, data) {
|
|
39
|
+
const cbs = this.callbacks.get(event);
|
|
40
|
+
if (cbs && cbs.length > 0) {
|
|
41
|
+
cbs.forEach(cb => {
|
|
42
|
+
try {
|
|
43
|
+
cb(data);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error(`[SovereignBroadcaster] Error in event listener for ${event}:`, e);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class SovereignBroadcasterClient {
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.endpoint = options.endpoint || '/api/broadcasting/events';
|
|
55
|
+
this.authEndpoint = options.authEndpoint || '/api/broadcasting/auth';
|
|
56
|
+
this.headers = options.headers || {};
|
|
57
|
+
this.channels = new Map();
|
|
58
|
+
this.socketId = this._generateSocketId();
|
|
59
|
+
this.lastEventId = 0;
|
|
60
|
+
this.reconnectAttempts = 0;
|
|
61
|
+
this.maxReconnectDelay = 15000;
|
|
62
|
+
this.isConnected = false;
|
|
63
|
+
|
|
64
|
+
if (typeof document !== 'undefined' && !this.headers['X-CSRF-TOKEN']) {
|
|
65
|
+
const metaCsrf = document.querySelector('meta[name="csrf-token"]');
|
|
66
|
+
if (metaCsrf && metaCsrf.content) {
|
|
67
|
+
this.headers['X-CSRF-TOKEN'] = metaCsrf.content;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_generateSocketId() {
|
|
73
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
74
|
+
const arr = new Uint32Array(2);
|
|
75
|
+
crypto.getRandomValues(arr);
|
|
76
|
+
return `${arr[0]}.${arr[1]}`;
|
|
77
|
+
}
|
|
78
|
+
const rand1 = Math.floor(Math.random() * 1000000);
|
|
79
|
+
const rand2 = Math.floor(Math.random() * 1000000);
|
|
80
|
+
return `${rand1}.${rand2}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
subscribe(channelName) {
|
|
84
|
+
if (typeof channelName !== 'string' || !/^[A-Za-z0-9_.:@-]{1,191}$/.test(channelName)) {
|
|
85
|
+
throw new Error('[SovereignBroadcaster] Invalid channel identifier');
|
|
86
|
+
}
|
|
87
|
+
if (this.channels.has(channelName)) {
|
|
88
|
+
return this.channels.get(channelName);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const channel = new SovereignChannel(channelName, this);
|
|
92
|
+
this.channels.set(channelName, channel);
|
|
93
|
+
|
|
94
|
+
if (channelName.startsWith('private-') || channelName.startsWith('presence-')) {
|
|
95
|
+
this._authorizeChannel(channelName)
|
|
96
|
+
.then(authData => {
|
|
97
|
+
channel.authData = authData || {};
|
|
98
|
+
channel.subscribed = true;
|
|
99
|
+
this._connectSse(channelName);
|
|
100
|
+
channel.emit('pusher:subscription_succeeded', authData);
|
|
101
|
+
})
|
|
102
|
+
.catch(err => {
|
|
103
|
+
console.error(`[SovereignBroadcaster] Authorization failed for ${channelName}:`, err);
|
|
104
|
+
channel.emit('pusher:subscription_error', err);
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
channel.subscribed = true;
|
|
108
|
+
this._connectSse(channelName);
|
|
109
|
+
channel.emit('pusher:subscription_succeeded', {});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return channel;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
unsubscribe(channelName) {
|
|
116
|
+
if (!this.channels.has(channelName)) return;
|
|
117
|
+
const channel = this.channels.get(channelName);
|
|
118
|
+
channel.subscribed = false;
|
|
119
|
+
this.channels.delete(channelName);
|
|
120
|
+
|
|
121
|
+
if (channel._eventSource) {
|
|
122
|
+
channel._eventSource.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_authorizeChannel(channelName) {
|
|
127
|
+
return fetch(this.authEndpoint, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
'Accept': 'application/json',
|
|
132
|
+
...this.headers
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
channel_name: channelName,
|
|
136
|
+
socket_id: this.socketId
|
|
137
|
+
})
|
|
138
|
+
}).then(res => {
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
throw new Error(`HTTP ${res.status} auth failure`);
|
|
141
|
+
}
|
|
142
|
+
return res.json();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_connectSse(channelName) {
|
|
147
|
+
const channel = this.channels.get(channelName);
|
|
148
|
+
if (!channel || typeof window === 'undefined' || typeof EventSource === 'undefined') return;
|
|
149
|
+
|
|
150
|
+
const origin = window.location ? window.location.origin : 'http://localhost';
|
|
151
|
+
const url = new URL(this.endpoint, origin);
|
|
152
|
+
url.searchParams.set('channel', channelName);
|
|
153
|
+
url.searchParams.set('socket_id', this.socketId);
|
|
154
|
+
if (channel.authData && channel.authData.auth) {
|
|
155
|
+
url.searchParams.set('auth', String(channel.authData.auth));
|
|
156
|
+
}
|
|
157
|
+
if (channel.authData && channel.authData.channel_data) {
|
|
158
|
+
url.searchParams.set('channel_data', String(channel.authData.channel_data));
|
|
159
|
+
}
|
|
160
|
+
if (this.lastEventId > 0) {
|
|
161
|
+
url.searchParams.set('last_event_id', String(this.lastEventId));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const es = new EventSource(url.toString());
|
|
165
|
+
channel._eventSource = es;
|
|
166
|
+
|
|
167
|
+
es.onopen = () => {
|
|
168
|
+
this.isConnected = true;
|
|
169
|
+
this.reconnectAttempts = 0;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
es.onmessage = (e) => {
|
|
173
|
+
if (e.lastEventId) {
|
|
174
|
+
this.lastEventId = parseInt(e.lastEventId, 10);
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(e.data);
|
|
178
|
+
const eventName = parsed.event || 'message';
|
|
179
|
+
const payload = parsed.data !== undefined ? parsed.data : parsed;
|
|
180
|
+
channel.emit(eventName, payload);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
channel.emit('message', e.data);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
es.onerror = () => {
|
|
187
|
+
this.isConnected = false;
|
|
188
|
+
es.close();
|
|
189
|
+
|
|
190
|
+
if (channel.subscribed) {
|
|
191
|
+
this.reconnectAttempts++;
|
|
192
|
+
const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), this.maxReconnectDelay);
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
if (channel.subscribed) {
|
|
195
|
+
this._connectSse(channelName);
|
|
196
|
+
}
|
|
197
|
+
}, delay);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
disconnect() {
|
|
203
|
+
this.channels.forEach(ch => {
|
|
204
|
+
ch.subscribed = false;
|
|
205
|
+
if (ch._eventSource) {
|
|
206
|
+
ch._eventSource.close();
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
this.channels.clear();
|
|
210
|
+
this.isConnected = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* HybridBroadcasterClient
|
|
216
|
+
*
|
|
217
|
+
* Provides primary Pusher routing with automatic, zero-downtime failover to
|
|
218
|
+
* SovereignBroadcaster if Pusher fails, disconnects, times out, or throws an error.
|
|
219
|
+
*/
|
|
220
|
+
export class HybridBroadcasterClient {
|
|
221
|
+
constructor(options = {}) {
|
|
222
|
+
this.pusherKey = options.pusherKey || options.key || '';
|
|
223
|
+
this.pusherOptions = options.pusherOptions || { cluster: options.cluster || 'eu' };
|
|
224
|
+
this.sovereignOptions = {
|
|
225
|
+
endpoint: options.endpoint || '/api/broadcasting/events',
|
|
226
|
+
authEndpoint: options.authEndpoint || '/api/broadcasting/auth',
|
|
227
|
+
headers: options.headers || {}
|
|
228
|
+
};
|
|
229
|
+
this.activeClient = null;
|
|
230
|
+
this.channels = new Map();
|
|
231
|
+
this.isUsingFallback = false;
|
|
232
|
+
this.connectTimeoutMs = options.connectTimeoutMs || 4000;
|
|
233
|
+
|
|
234
|
+
this._initTransport();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_initTransport() {
|
|
238
|
+
const hasPusher = typeof window !== 'undefined' && typeof window.Pusher === 'function' && this.pusherKey !== '';
|
|
239
|
+
if (hasPusher) {
|
|
240
|
+
try {
|
|
241
|
+
const pusherInstance = new window.Pusher(this.pusherKey, this.pusherOptions);
|
|
242
|
+
this.activeClient = pusherInstance;
|
|
243
|
+
|
|
244
|
+
if (pusherInstance.connection) {
|
|
245
|
+
pusherInstance.connection.bind('error', (err) => {
|
|
246
|
+
console.warn('[HybridBroadcaster] Pusher error encountered, failing over to SovereignBroadcaster:', err);
|
|
247
|
+
this._failoverToSovereign();
|
|
248
|
+
});
|
|
249
|
+
pusherInstance.connection.bind('unavailable', () => {
|
|
250
|
+
console.warn('[HybridBroadcaster] Pusher unavailable, failing over to SovereignBroadcaster');
|
|
251
|
+
this._failoverToSovereign();
|
|
252
|
+
});
|
|
253
|
+
pusherInstance.connection.bind('failed', () => {
|
|
254
|
+
console.warn('[HybridBroadcaster] Pusher connection failed, failing over to SovereignBroadcaster');
|
|
255
|
+
this._failoverToSovereign();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
if (pusherInstance.connection.state !== 'connected' && !this.isUsingFallback) {
|
|
260
|
+
console.warn('[HybridBroadcaster] Pusher connection timeout exceeded, failing over to SovereignBroadcaster');
|
|
261
|
+
this._failoverToSovereign();
|
|
262
|
+
}
|
|
263
|
+
}, this.connectTimeoutMs);
|
|
264
|
+
}
|
|
265
|
+
} catch (e) {
|
|
266
|
+
console.warn('[HybridBroadcaster] Failed to initialise Pusher, defaulting to SovereignBroadcaster:', e);
|
|
267
|
+
this._failoverToSovereign();
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
this._failoverToSovereign();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_failoverToSovereign() {
|
|
275
|
+
if (this.isUsingFallback) return;
|
|
276
|
+
this.isUsingFallback = true;
|
|
277
|
+
const sovereign = new SovereignBroadcasterClient(this.sovereignOptions);
|
|
278
|
+
this.activeClient = sovereign;
|
|
279
|
+
|
|
280
|
+
this.channels.forEach((channelData, channelName) => {
|
|
281
|
+
const sovChannel = sovereign.subscribe(channelName);
|
|
282
|
+
channelData.events.forEach((callbacks, eventName) => {
|
|
283
|
+
callbacks.forEach(cb => sovChannel.bind(eventName, cb));
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
subscribe(channelName) {
|
|
289
|
+
if (!this.channels.has(channelName)) {
|
|
290
|
+
this.channels.set(channelName, { events: new Map() });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const channelProxy = {
|
|
294
|
+
name: channelName,
|
|
295
|
+
bind: (event, callback) => {
|
|
296
|
+
const chData = this.channels.get(channelName);
|
|
297
|
+
if (!chData.events.has(event)) {
|
|
298
|
+
chData.events.set(event, []);
|
|
299
|
+
}
|
|
300
|
+
chData.events.get(event).push(callback);
|
|
301
|
+
|
|
302
|
+
if (this.activeClient && typeof this.activeClient.subscribe === 'function') {
|
|
303
|
+
const sub = this.activeClient.subscribe(channelName);
|
|
304
|
+
if (sub && typeof sub.bind === 'function') {
|
|
305
|
+
sub.bind(event, callback);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return channelProxy;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (this.activeClient && typeof this.activeClient.subscribe === 'function') {
|
|
313
|
+
this.activeClient.subscribe(channelName);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return channelProxy;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
disconnect() {
|
|
320
|
+
if (this.activeClient && typeof this.activeClient.disconnect === 'function') {
|
|
321
|
+
this.activeClient.disconnect();
|
|
322
|
+
}
|
|
323
|
+
this.channels.clear();
|
|
324
|
+
}
|
|
325
|
+
}
|