@joryio/web-sdk 0.0.0-stage → 1.3.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +54 -0
  3. package/README.md +497 -2
  4. package/dist/core/config.d.ts +101 -0
  5. package/dist/core/config.d.ts.map +1 -0
  6. package/dist/core/device.d.ts +14 -0
  7. package/dist/core/device.d.ts.map +1 -0
  8. package/dist/core/ecommerce.d.ts +67 -0
  9. package/dist/core/ecommerce.d.ts.map +1 -0
  10. package/dist/core/identity.d.ts +36 -0
  11. package/dist/core/identity.d.ts.map +1 -0
  12. package/dist/core/inapp.d.ts +169 -0
  13. package/dist/core/inapp.d.ts.map +1 -0
  14. package/dist/core/network.d.ts +158 -0
  15. package/dist/core/network.d.ts.map +1 -0
  16. package/dist/core/outbox.d.ts +79 -0
  17. package/dist/core/outbox.d.ts.map +1 -0
  18. package/dist/core/pending-attributes.d.ts +43 -0
  19. package/dist/core/pending-attributes.d.ts.map +1 -0
  20. package/dist/core/push.d.ts +59 -0
  21. package/dist/core/push.d.ts.map +1 -0
  22. package/dist/core/queue.d.ts +107 -0
  23. package/dist/core/queue.d.ts.map +1 -0
  24. package/dist/core/sdk.d.ts +169 -0
  25. package/dist/core/sdk.d.ts.map +1 -0
  26. package/dist/core/session.d.ts +36 -0
  27. package/dist/core/session.d.ts.map +1 -0
  28. package/dist/core/storage.d.ts +17 -0
  29. package/dist/core/storage.d.ts.map +1 -0
  30. package/dist/core/user.d.ts +28 -0
  31. package/dist/core/user.d.ts.map +1 -0
  32. package/dist/core/utm.d.ts +39 -0
  33. package/dist/core/utm.d.ts.map +1 -0
  34. package/dist/core/values.d.ts +3 -0
  35. package/dist/core/values.d.ts.map +1 -0
  36. package/dist/core/write-retry.d.ts +66 -0
  37. package/dist/core/write-retry.d.ts.map +1 -0
  38. package/dist/index.d.mts +711 -0
  39. package/dist/index.d.ts +21 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/joryio-sw.js +6 -0
  42. package/dist/joryio.cjs.js +7265 -0
  43. package/dist/joryio.min.js +7 -0
  44. package/dist/joryio.min.js.map +1 -0
  45. package/dist/joryio.mjs +7254 -0
  46. package/dist/joryio.umd.js +7271 -0
  47. package/dist/utils/bucket-hash.d.ts +2 -0
  48. package/dist/utils/bucket-hash.d.ts.map +1 -0
  49. package/dist/utils/logger.d.ts +14 -0
  50. package/dist/utils/logger.d.ts.map +1 -0
  51. package/dist/utils/uuid.d.ts +3 -0
  52. package/dist/utils/uuid.d.ts.map +1 -0
  53. package/package.json +90 -4
  54. package/src/core/config.ts +362 -0
  55. package/src/core/device.ts +50 -0
  56. package/src/core/ecommerce.ts +482 -0
  57. package/src/core/identity.ts +251 -0
  58. package/src/core/inapp.ts +2419 -0
  59. package/src/core/network.ts +1261 -0
  60. package/src/core/outbox.ts +707 -0
  61. package/src/core/pending-attributes.ts +350 -0
  62. package/src/core/push.ts +667 -0
  63. package/src/core/queue.ts +894 -0
  64. package/src/core/sdk.ts +3108 -0
  65. package/src/core/session.ts +311 -0
  66. package/src/core/storage.ts +157 -0
  67. package/src/core/user.ts +203 -0
  68. package/src/core/utm.ts +304 -0
  69. package/src/core/values.ts +85 -0
  70. package/src/core/write-retry.ts +136 -0
  71. package/src/index.ts +305 -0
  72. package/src/snippet/loader.js +134 -0
  73. package/src/utils/bucket-hash.ts +44 -0
  74. package/src/utils/logger.ts +58 -0
  75. package/src/utils/uuid.ts +29 -0
  76. package/src/worker/joryio-sw.js +218 -0
package/src/index.ts ADDED
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Joryio Web SDK
3
+ * Main entry point
4
+ */
5
+
6
+ import { JoryioSDK, PRE_INIT_BASELINE, runWithDataCallsDropped, DataGateReason } from './core/sdk';
7
+ import type { TrackingConsent, PreInitDecision } from './core/sdk';
8
+ import type { InitConfig, Plugin, InAppOptions, InAppDisplayOptions } from './core/config';
9
+ import type { UTMParameters, UTMData } from './core/utm';
10
+ import type { InAppCampaign, InAppMessage, InAppTrigger } from './core/inapp';
11
+ import type {
12
+ SdkAuthFailureInfo,
13
+ SdkAuthFailureReason,
14
+ SdkAuthFailureCallback,
15
+ } from './core/network';
16
+ import { User, SubscriptionStatus } from './core/user';
17
+ import type { SubscriptionChannel, SubscriptionPreferences } from './core/user';
18
+ import { PushManager } from './core/push';
19
+ import type { PushSubscribeOptions, PushPermissionState } from './core/push';
20
+ import { EcommerceTracker, createEcommerceTracker } from './core/ecommerce';
21
+ import type {
22
+ EcommerceProduct,
23
+ EcommerceCartItem,
24
+ EcommerceOrder,
25
+ EcommerceConfig,
26
+ } from './core/ecommerce';
27
+
28
+ // Export main SDK class
29
+ export { JoryioSDK };
30
+
31
+ // Export User class and subscription types
32
+ export { User, SubscriptionStatus };
33
+
34
+ // Export Push manager
35
+ export { PushManager };
36
+
37
+ // Export E-commerce tracker
38
+ export { EcommerceTracker, createEcommerceTracker };
39
+
40
+ // Export types
41
+ export type {
42
+ InitConfig,
43
+ Plugin,
44
+ InAppOptions,
45
+ InAppDisplayOptions,
46
+ InAppCampaign,
47
+ InAppMessage,
48
+ InAppTrigger,
49
+ UTMParameters,
50
+ UTMData,
51
+ SubscriptionChannel,
52
+ SubscriptionPreferences,
53
+ PushSubscribeOptions,
54
+ PushPermissionState,
55
+ TrackingConsent,
56
+ EcommerceProduct,
57
+ EcommerceCartItem,
58
+ EcommerceOrder,
59
+ EcommerceConfig,
60
+ SdkAuthFailureInfo,
61
+ SdkAuthFailureReason,
62
+ SdkAuthFailureCallback,
63
+ };
64
+
65
+ // Export SDK version
66
+ export { SDK_VERSION } from './core/config';
67
+
68
+ // Default export for convenience
69
+ export default JoryioSDK;
70
+
71
+ /**
72
+ * Wire the async snippet's command queue to the SDK.
73
+ *
74
+ * The snippet makes `window.joryio` an array whose stub methods push
75
+ * `[method, ...args]` onto it. When this bundle loads:
76
+ * - if an `init` is already queued, the SDK is created, `window.joryio`
77
+ * becomes the SDK instance, and every other queued call is replayed in
78
+ * order;
79
+ * - if not (a site that calls `joryio.init()` later - after its consent
80
+ * banner, say), the queue keeps collecting and the SDK starts the moment
81
+ * `init` is pushed. That case used to be ignored: the late init sat in the
82
+ * array forever and nothing was ever tracked.
83
+ *
84
+ * This used to also assign `window.joryio.push = ...` on the SDK instance,
85
+ * where `push` is the read-only web-push getter. In the (strict-mode) bundle
86
+ * that assignment THREW - "Cannot set property push of #<L> which has only a
87
+ * getter" - on every page using the snippet, aborting the UMD factory before
88
+ * it exported anything. Late calls need no override: the stubs push onto the
89
+ * original array (handled below), and calls on `window.joryio` after load go
90
+ * straight to the SDK's own methods.
91
+ *
92
+ * Exported for tests. Never throws into the host page.
93
+ */
94
+ /** Calls held before `init` (snippet queue). The same cap on every Joryio SDK. */
95
+ const MAX_PRE_INIT_CALLS = 1000;
96
+ /** Never dropped by that cap: `init` and the visitor's privacy decisions (rule 51). */
97
+ const KEPT_PRE_INIT_CALLS = ['init', 'optOut', 'optIn', 'updateConsent', 'wipeData'];
98
+ /**
99
+ * Queued calls that COLLECT data (rule 73): one made while the visitor was
100
+ * opted out or consent was denied - by the stored state, the init config or
101
+ * a decision queued before it - is dropped, not replayed after a later
102
+ * queued optIn() or grant. identify() is not one (a user kept for
103
+ * withdrawals, D-b), nor are withdrawals and control calls.
104
+ */
105
+ const DATA_CALLS = [
106
+ 'track',
107
+ 'setAttributes',
108
+ 'setAttribute',
109
+ 'incrementAttribute',
110
+ 'unsetAttribute',
111
+ 'addToArray',
112
+ 'removeFromArray',
113
+ 'addAlias',
114
+ 'trackInAppConversion',
115
+ ];
116
+
117
+ /**
118
+ * Which queued data calls (DATA_CALLS) and ready() callbacks were made while
119
+ * tracking was off - or before a queued optOut(), updateConsent('denied') or
120
+ * pre-init wipeData() (rule 83, the pre-init purge: the person withdrew what
121
+ * was queued before it, and a later queued optIn() or grant does not bring it
122
+ * back). The data calls are dropped; such a ready() callback still runs, with
123
+ * the data calls it makes dropped (rule 83) - and each callback carries WHY
124
+ * (rule 92), so only the matching decision inside it lifts the gate. A
125
+ * pre-init wipeData() gates no ready() callback by itself: the callback runs
126
+ * after it, for the fresh visitor.
127
+ */
128
+ function callsMadeWhileOff(
129
+ commands: any[],
130
+ baseline: { optedOut: boolean; consent: string } | null | undefined,
131
+ initIndex: number = commands.length,
132
+ reasons: Map<any, Set<DataGateReason>> = new Map(),
133
+ ): Set<any> {
134
+ const off = new Set<any>();
135
+ // `reasons` (filled for the caller): why each one is off (rule 92).
136
+ const gate = (command: any, reason: DataGateReason) => {
137
+ off.add(command);
138
+ const why = reasons.get(command) || new Set<DataGateReason>();
139
+ why.add(reason);
140
+ reasons.set(command, why);
141
+ };
142
+ let optedOut = !!baseline?.optedOut;
143
+ let consent = baseline?.consent || 'granted';
144
+ const collecting: any[] = [];
145
+ const purge = (byWipe = false) => {
146
+ const kept = collecting.filter((command) => byWipe && command[0] === 'ready');
147
+ collecting.forEach((command) => {
148
+ if (!kept.includes(command)) gate(command, 'purged');
149
+ });
150
+ collecting.length = 0;
151
+ collecting.push(...kept);
152
+ };
153
+ commands.forEach((command, index) => {
154
+ if (!Array.isArray(command)) return;
155
+ const [method, arg] = command;
156
+ if (method === 'optOut') {
157
+ optedOut = true;
158
+ purge();
159
+ } else if (method === 'optIn') optedOut = false;
160
+ else if (method === 'updateConsent') {
161
+ if (arg === 'granted' || arg === 'denied') consent = arg;
162
+ // 'pending' never reopens a denial.
163
+ else if (arg === 'pending' && consent !== 'denied') consent = 'pending';
164
+ if (arg === 'denied') purge();
165
+ } else if (method === 'wipeData' && index < initIndex) {
166
+ // Applied inside init, before the replay: what was queued before it
167
+ // would otherwise be collected for the fresh visitor the wipe makes.
168
+ purge(true);
169
+ } else if (DATA_CALLS.includes(method) || method === 'ready') {
170
+ if (optedOut) gate(command, 'optedOut');
171
+ if (consent === 'denied') gate(command, 'denied');
172
+ if (!optedOut && consent !== 'denied') collecting.push(command);
173
+ }
174
+ });
175
+ return off;
176
+ }
177
+
178
+ export function installSnippetQueue(win: any): void {
179
+ try {
180
+ const queue = win && win.joryio;
181
+ if (!Array.isArray(queue)) return;
182
+
183
+ const initCommand = queue.find((cmd: any) => Array.isArray(cmd) && cmd[0] === 'init');
184
+ if (!initCommand) {
185
+ if ((queue as any).__joryioWaitingForInit) return;
186
+ (queue as any).__joryioWaitingForInit = true;
187
+ // An own property on the array instance - assignable, unlike the SDK's getter.
188
+ (queue as any).push = function (...commands: any[]) {
189
+ // Same pre-init cap as the snippet (and every Joryio SDK): 1000
190
+ // calls, oldest dropped first - never `init` nor a privacy decision.
191
+ for (let overflow = queue.length + commands.length - MAX_PRE_INIT_CALLS; overflow > 0; overflow--) {
192
+ const oldest = queue.findIndex((cmd: any) => !(Array.isArray(cmd) && KEPT_PRE_INIT_CALLS.includes(cmd[0])));
193
+ if (oldest < 0) break;
194
+ queue.splice(oldest, 1);
195
+ }
196
+ const length = Array.prototype.push.apply(queue, commands);
197
+ if (commands.some((cmd) => Array.isArray(cmd) && cmd[0] === 'init')) {
198
+ installSnippetQueue(win);
199
+ }
200
+ return length;
201
+ };
202
+ return;
203
+ }
204
+
205
+ // Privacy decisions queued before the bundle loaded take effect INSIDE
206
+ // init, before any of its side effects (contract item 25). Replayed
207
+ // afterwards, as they used to be, they came too late: `optOut()` or
208
+ // `updateConsent('denied')` queued ahead of `init` still let init send
209
+ // Session Start, sync in-app messages and re-register push first.
210
+ //
211
+ // optOut / optIn / updateConsent are folded from the WHOLE queue (all of
212
+ // it predates the SDK, and replaying, say, an identify under a consent
213
+ // that a later queued call grants would drop it). wipeData only when it
214
+ // came before init: after init it is ordered against the calls around it.
215
+ const commands = queue.slice();
216
+ const initIndex = commands.indexOf(initCommand);
217
+ const preInit: PreInitDecision[] = [];
218
+ const folded = new Set<any>();
219
+ commands.forEach((command: any, index: number) => {
220
+ if (!Array.isArray(command)) return;
221
+ const [method, arg] = command;
222
+ if (method === 'optOut' || method === 'optIn') {
223
+ preInit.push({ method });
224
+ } else if (method === 'updateConsent') {
225
+ preInit.push({ method, consent: arg });
226
+ } else if (method === 'wipeData' && index < initIndex) {
227
+ preInit.push({ method });
228
+ } else {
229
+ return;
230
+ }
231
+ folded.add(command);
232
+ });
233
+
234
+ let sdk: JoryioSDK | null = null;
235
+ try {
236
+ sdk = new JoryioSDK(initCommand[1], preInit);
237
+ } catch (error) {
238
+ // eslint-disable-next-line no-console -- no SDK instance (and so no logger) exists when init itself fails
239
+ console.error('[Joryio] init failed:', error);
240
+ }
241
+ if (!sdk || JoryioSDK.getInstance() !== sdk) {
242
+ // The init failed (a missing or malformed key builds an inert
243
+ // instance). Nothing queued is replayed into it - an unsubscribe or an
244
+ // optOut() would be swallowed - and `window.joryio` stays the queue:
245
+ // every call, privacy decisions and withdrawals included, is KEPT in
246
+ // order, and a corrected `joryio.init(...)` starts the SDK with them
247
+ // (decisions first, rule 65). The failed init is dropped so it is not
248
+ // tried again.
249
+ const at = queue.indexOf(initCommand);
250
+ if (at >= 0) queue.splice(at, 1);
251
+ (queue as any).__joryioWaitingForInit = false;
252
+ installSnippetQueue(win);
253
+ return;
254
+ }
255
+
256
+ // Data calls made while tracking was off are dropped, whatever a later
257
+ // queued decision says (rule 73).
258
+ const gateReasons = new Map<any, Set<DataGateReason>>();
259
+ const madeWhileOff = callsMadeWhileOff(commands, PRE_INIT_BASELINE.get(sdk), initIndex, gateReasons);
260
+ // Swap first, so a replayed ready(cb) - or anything it calls - sees the SDK.
261
+ win.joryio = sdk;
262
+ for (const command of commands) {
263
+ if (!Array.isArray(command) || folded.has(command)) continue;
264
+ const [method, ...args] = command;
265
+ if (method === 'init') continue;
266
+ const gated = madeWhileOff.has(command);
267
+ if (gated && method !== 'ready') {
268
+ (sdk as any).logger?.debug?.(`${String(method)} queued before load dropped: tracking was off when it was called`);
269
+ continue;
270
+ }
271
+ const fn = (sdk as any)[method];
272
+ if (typeof fn !== 'function') {
273
+ // eslint-disable-next-line no-console -- surfaced regardless of log level: a typo in a queued call is otherwise invisible
274
+ console.warn(`[Joryio] Unknown method queued before load: ${String(method)}`);
275
+ continue;
276
+ }
277
+ try {
278
+ let result: any;
279
+ // A ready() callback queued while tracking was off runs at the state
280
+ // of its queue position: the data calls it makes are dropped (rule 83).
281
+ if (gated) runWithDataCallsDropped(sdk, gateReasons.get(command) || [], () => (result = fn.apply(sdk, args)));
282
+ else result = fn.apply(sdk, args);
283
+ if (result && typeof result.catch === 'function') {
284
+ result.catch((error: unknown) => {
285
+ // eslint-disable-next-line no-console -- see above
286
+ console.error(`[Joryio] Error executing ${String(method)}:`, error);
287
+ });
288
+ }
289
+ } catch (error) {
290
+ // eslint-disable-next-line no-console -- see above
291
+ console.error(`[Joryio] Error executing ${String(method)}:`, error);
292
+ }
293
+ }
294
+ } catch (error) {
295
+ // eslint-disable-next-line no-console -- see above
296
+ console.error('[Joryio] Could not process the snippet queue:', error);
297
+ }
298
+ }
299
+
300
+ // Browser global initialization
301
+ if (typeof window !== 'undefined') {
302
+ // Make SDK available on window object as 'Joryio'
303
+ (window as any).Joryio = JoryioSDK;
304
+ installSnippetQueue(window);
305
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Joryio Web SDK - Async Loader Snippet
3
+ *
4
+ * This snippet should be embedded in the <head> of your website.
5
+ * It creates a command queue pattern that allows you to call the SDK
6
+ * before the full SDK is loaded.
7
+ *
8
+ * Usage:
9
+ * <script>
10
+ * !function(){var e=window.joryio=window.joryio||[];if(!e.initialize)if(e.invoked)window.console&&console.error&&console.error("Joryio snippet included twice.");else{e.invoked=!0,e.methods=["init","track","identify","addAlias","setAttributes","setAttribute","incrementAttribute","unsetAttribute","addToArray","removeFromArray","reset","flush","optOut","optIn","updateConsent","getTrackingConsent","isUserOptedOut","setSdkAuthenticationToken","setSdkAuthenticationErrorCallback","getAnonymousId","getUserId","ready","getIdentity","getSessionInfo","getUTMData","updateUTM","syncInAppCampaigns","getInAppCampaigns","displayInAppCampaign","dismissInAppMessage","trackInAppConversion","ecommerce","wipeData","destroy"],e.keep=["init","optOut","optIn","updateConsent","wipeData"],e.factory=function(t){return function(){var n=Array.prototype.slice.call(arguments);if(n.unshift(t),e.length>=1e3)for(var i=0;i<e.length;i++)if(e.keep.indexOf(e[i][0])<0){e.splice(i,1);break}return e.push(n),/^(get|is)[A-Z]/.test(t)?void 0:e}};for(var t=0;t<e.methods.length;t++){var n=e.methods[t];e[n]=e.factory(n)}e.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://sdk.joryio.com/web/1/joryio.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n)},e.load()}}();
11
+ * joryio.init({ sdkKey: 'YOUR_SDK_KEY' });
12
+ * </script>
13
+ *
14
+ * `init` may also come later (for example from a consent banner's callback):
15
+ * calls made before it are kept and run, in order, once it arrives.
16
+ *
17
+ * Reading a value: the queued stubs above cannot return anything before the
18
+ * SDK finishes loading - a getter (getUserId(), isUserOptedOut(), ...) returns
19
+ * `undefined` until then, never a truthy placeholder - so read values in
20
+ * ready(), or on the resolved instance after load. The same goes for `user`,
21
+ * `push` and the tracker `ecommerce()` returns:
22
+ *
23
+ * joryio.ready(function (sdk) {
24
+ * var anonId = sdk.getAnonymousId();
25
+ * var userId = sdk.getUserId(); // null until identify()
26
+ * sdk.user.setSubscription('email', 'optedIn');
27
+ * });
28
+ *
29
+ * The readable version is below for reference. Keep the two in step: a test
30
+ * (src/snippet/loader.spec.ts) checks that both list every public SDK method
31
+ * and that the README carries the minified line unchanged.
32
+ */
33
+
34
+ (function() {
35
+ // Create the joryio queue
36
+ var joryio = window.joryio = window.joryio || [];
37
+
38
+ // If already initialized, skip
39
+ if (!joryio.initialize) {
40
+ if (joryio.invoked) {
41
+ window.console && console.error && console.error('Joryio snippet included twice.');
42
+ } else {
43
+ joryio.invoked = true;
44
+
45
+ // Every public method of the SDK instance (src/core/sdk.ts), so no call
46
+ // made before load throws "is not a function". Methods that RETURN a
47
+ // value can only do so after load - see ready() above.
48
+ joryio.methods = [
49
+ 'init',
50
+ 'track',
51
+ 'identify',
52
+ 'addAlias',
53
+ 'setAttributes',
54
+ 'setAttribute',
55
+ 'incrementAttribute',
56
+ 'unsetAttribute',
57
+ 'addToArray',
58
+ 'removeFromArray',
59
+ 'reset',
60
+ 'flush',
61
+ 'optOut',
62
+ 'optIn',
63
+ 'updateConsent',
64
+ 'getTrackingConsent',
65
+ 'isUserOptedOut',
66
+ 'setSdkAuthenticationToken',
67
+ 'setSdkAuthenticationErrorCallback',
68
+ 'getAnonymousId',
69
+ 'getUserId',
70
+ 'ready',
71
+ 'getIdentity',
72
+ 'getSessionInfo',
73
+ 'getUTMData',
74
+ 'updateUTM',
75
+ 'syncInAppCampaigns',
76
+ 'getInAppCampaigns',
77
+ 'displayInAppCampaign',
78
+ 'dismissInAppMessage',
79
+ 'trackInAppConversion',
80
+ 'ecommerce',
81
+ 'wipeData',
82
+ 'destroy'
83
+ ];
84
+
85
+ // Calls the cap below never drops: `init`, and the visitor's privacy
86
+ // decisions (rule 51) - an optOut() lost to a burst of track() calls
87
+ // would let the SDK start tracking someone who said no.
88
+ joryio.keep = ['init', 'optOut', 'optIn', 'updateConsent', 'wipeData'];
89
+
90
+ // Factory for creating queue methods. The queue holds at most 1000
91
+ // calls - the pre-init cap every Joryio SDK uses - dropping the OLDEST
92
+ // first, but never one in `keep`: a page that never loads the bundle
93
+ // (blocked script) must not grow it without bound.
94
+ joryio.factory = function(method) {
95
+ return function() {
96
+ var args = Array.prototype.slice.call(arguments);
97
+ args.unshift(method);
98
+ if (joryio.length >= 1000) {
99
+ for (var j = 0; j < joryio.length; j++) {
100
+ if (joryio.keep.indexOf(joryio[j][0]) < 0) {
101
+ joryio.splice(j, 1);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ joryio.push(args);
107
+ // A getter has no answer before load: undefined, not the queue (an
108
+ // array is truthy, so `if (joryio.isUserOptedOut())` was wrong).
109
+ return /^(get|is)[A-Z]/.test(method) ? undefined : joryio;
110
+ };
111
+ };
112
+
113
+ // Create queue methods
114
+ for (var i = 0; i < joryio.methods.length; i++) {
115
+ var key = joryio.methods[i];
116
+ joryio[key] = joryio.factory(key);
117
+ }
118
+
119
+ // Load the SDK asynchronously
120
+ joryio.load = function() {
121
+ var script = document.createElement('script');
122
+ script.type = 'text/javascript';
123
+ script.async = true;
124
+ script.src = 'https://sdk.joryio.com/web/1/joryio.min.js';
125
+
126
+ var first = document.getElementsByTagName('script')[0];
127
+ first.parentNode.insertBefore(script, first);
128
+ };
129
+
130
+ // Auto-load
131
+ joryio.load();
132
+ }
133
+ }
134
+ })();
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The same 32-bit hash the backend uses, kept byte-for-byte identical.
3
+ *
4
+ * MIRRORS packages/backend/src/common/utils/bucket-hash.util.ts. A backend
5
+ * tripwire (sdk-web-hash-parity.spec.ts) fails if the two drift, because a hash
6
+ * that is "nearly" the same is worse than an obviously different one - it
7
+ * agrees on most inputs and disagrees on the ones nobody tested.
8
+ *
9
+ * FNV-1a XORs each byte in before multiplying, so every input bit reaches the
10
+ * whole word. Its last operation is a multiply though, and multiplication only
11
+ * ever carries LEFT - nothing propagates down into the low bits. Anything
12
+ * reading those (a `% n`) sees a biased digest, so MurmurHash3's fmix32
13
+ * finalizer folds the high bits back down.
14
+ *
15
+ * NOT for anything security-bearing. It is a fast non-cryptographic hash.
16
+ */
17
+ const FNV_OFFSET_BASIS = 2166136261;
18
+ const FNV_PRIME = 16777619;
19
+
20
+ /** MurmurHash3's 32-bit finalizer - see the note above about low bits. */
21
+ function fmix32(h: number): number {
22
+ h ^= h >>> 16;
23
+ h = Math.imul(h, 0x85ebca6b);
24
+ h ^= h >>> 13;
25
+ h = Math.imul(h, 0xc2b2ae35);
26
+ h ^= h >>> 16;
27
+ return h >>> 0;
28
+ }
29
+
30
+ /** 32-bit FNV-1a over the UTF-16 code units, finalized, returned unsigned. */
31
+ export function bucketHash(input: string): number {
32
+ let hash = FNV_OFFSET_BASIS;
33
+ for (let i = 0; i < input.length; i++) {
34
+ const code = input.charCodeAt(i);
35
+ // Both bytes of the code unit, so 'Ā' and 'A' cannot collide by truncation.
36
+ hash ^= code & 0xff;
37
+ hash = Math.imul(hash, FNV_PRIME);
38
+ hash ^= (code >>> 8) & 0xff;
39
+ hash = Math.imul(hash, FNV_PRIME);
40
+ }
41
+ // >>> 0 rather than Math.abs: Math.abs(-2147483648) is not a 32-bit int, and
42
+ // negating loses the top bit's contribution.
43
+ return fmix32(hash >>> 0);
44
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Simple logger utility for SDK debugging
3
+ */
4
+
5
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
6
+
7
+ const LOG_LEVELS: Record<LogLevel, number> = {
8
+ error: 0,
9
+ warn: 1,
10
+ info: 2,
11
+ debug: 3,
12
+ };
13
+
14
+ export class Logger {
15
+ private enabled: boolean;
16
+ private level: LogLevel;
17
+
18
+ constructor(enabled: boolean, level: LogLevel = 'error') {
19
+ this.enabled = enabled;
20
+ this.level = level;
21
+ }
22
+
23
+ private shouldLog(level: LogLevel): boolean {
24
+ return this.enabled && LOG_LEVELS[level] <= LOG_LEVELS[this.level];
25
+ }
26
+
27
+ error(...args: any[]): void {
28
+ if (this.shouldLog('error')) {
29
+ console.error('[Joryio SDK]', ...args);
30
+ }
31
+ }
32
+
33
+ warn(...args: any[]): void {
34
+ if (this.shouldLog('warn')) {
35
+ console.warn('[Joryio SDK]', ...args);
36
+ }
37
+ }
38
+
39
+ info(...args: any[]): void {
40
+ if (this.shouldLog('info')) {
41
+ console.info('[Joryio SDK]', ...args);
42
+ }
43
+ }
44
+
45
+ debug(...args: any[]): void {
46
+ if (this.shouldLog('debug')) {
47
+ console.debug('[Joryio SDK]', ...args);
48
+ }
49
+ }
50
+
51
+ setEnabled(enabled: boolean): void {
52
+ this.enabled = enabled;
53
+ }
54
+
55
+ setLevel(level: LogLevel): void {
56
+ this.level = level;
57
+ }
58
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Generate UUID v4
3
+ * Lightweight implementation without dependencies
4
+ */
5
+ export function generateUUID(): string {
6
+ // Use crypto.randomUUID if available (modern browsers)
7
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
8
+ return crypto.randomUUID();
9
+ }
10
+
11
+ // Fallback to Math.random based UUID v4
12
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
13
+ const r = (Math.random() * 16) | 0;
14
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
15
+ return v.toString(16);
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Generate a short unique ID (for session IDs, etc.)
21
+ */
22
+ export function generateShortId(): string {
23
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
24
+ const arr = new Uint8Array(6);
25
+ crypto.getRandomValues(arr);
26
+ return Date.now().toString(36) + Array.from(arr, x => x.toString(16).padStart(2, '0')).join('');
27
+ }
28
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
29
+ }