@sorisdk/web-audio 0.6.2 → 0.6.4

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/README.md CHANGED
@@ -13,7 +13,96 @@ Public integration guide: https://docs.soriapi.com/ko/integration/web
13
13
  - An ephemeral key issued by your server
14
14
  - Microphone permission granted by the user
15
15
 
16
- ## Installation
16
+ ## Standalone CDN
17
+
18
+ For a browser application that does not use npm or a bundler, import the
19
+ versioned standalone module directly. No package installation, Vite/webpack
20
+ configuration, or Node-based asset server is required:
21
+
22
+ ```html
23
+ <script type="module">
24
+ import { AudioRecognizer } from
25
+ "https://cdn.iplateia.com/web/sorisdk/v0.6.4/sori-web-audio.mjs";
26
+
27
+ const recognizer = new AudioRecognizer({
28
+ appId: "YOUR_APP_ID",
29
+ ephemeralKey: async () => {
30
+ const response = await fetch("/api/ephemeral-key", {
31
+ method: "POST"
32
+ });
33
+
34
+ if (!response.ok) {
35
+ throw new Error(`Ephemeral key request failed: HTTP ${response.status}`);
36
+ }
37
+
38
+ const { ephemeral_key } = await response.json();
39
+ return ephemeral_key;
40
+ }
41
+ });
42
+
43
+ recognizer.on("campaign", (event) => {
44
+ console.log(event.campaign);
45
+ });
46
+
47
+ await recognizer.start();
48
+ </script>
49
+ ```
50
+
51
+ Static URL imports are valid inside `<script type="module">`. Dynamic
52
+ `await import("https://cdn.iplateia.com/web/sorisdk/v0.6.4/sori-web-audio.mjs")`
53
+ is an optional alternative when the SDK should be loaded conditionally.
54
+
55
+ Pin an exact version in production. Do not construct a mutable `latest` URL.
56
+ The module loads its versioned WASM assets and the shared
57
+ `https://cdn.iplateia.com/web/sorisdk/model.pack` automatically.
58
+
59
+ The standalone module removes the frontend build/server requirement, but it
60
+ does not remove the trusted server requirement for ephemeral credentials.
61
+ Provide a backend or serverless endpoint for the `ephemeralKey` callback, and
62
+ never place a long-lived application secret in browser code.
63
+
64
+ The application operator is responsible for protecting that endpoint with the
65
+ authentication or anonymous-user verification appropriate to the product,
66
+ plus per-user/IP/session rate limits, issuance quotas, and abuse monitoring. A
67
+ public endpoint that returns a reusable key to every POST request can let an
68
+ anonymous caller consume recognition quota. CORS is required when the endpoint
69
+ is on a different origin, but CORS is not authentication and does not stop
70
+ scripts, bots, or non-browser clients from calling the endpoint.
71
+
72
+ Production billing and abuse protection must also rely on server-enforced key
73
+ expiry, app/session scope, revocation, and usage quotas. Do not treat the word
74
+ “ephemeral” or secrecy of a browser-visible key as the security boundary.
75
+
76
+ For a strict Content Security Policy, allow WebAssembly compilation with
77
+ `'wasm-unsafe-eval'`, allow `https://cdn.iplateia.com` in `script-src` and
78
+ `connect-src`, and add the SORI API plus ephemeral-key endpoint origins to
79
+ `connect-src`. For example:
80
+
81
+ ```text
82
+ script-src 'self' 'wasm-unsafe-eval' https://cdn.iplateia.com;
83
+ connect-src 'self' https://cdn.iplateia.com https://console.soriapi.com;
84
+ ```
85
+
86
+ If the module script is inline as in the example above, authorize that script
87
+ with a nonce or hash (or move it into an allowed external script) instead of
88
+ adding `'unsafe-inline'`.
89
+
90
+ An import map can give the standalone URL the npm package name:
91
+
92
+ ```html
93
+ <script type="importmap">
94
+ {
95
+ "imports": {
96
+ "@sorisdk/web-audio": "https://cdn.iplateia.com/web/sorisdk/v0.6.4/sori-web-audio.mjs"
97
+ }
98
+ }
99
+ </script>
100
+ <script type="module">
101
+ import { AudioRecognizer } from "@sorisdk/web-audio";
102
+ </script>
103
+ ```
104
+
105
+ ## npm installation
17
106
 
18
107
  ```bash
19
108
  npm install @sorisdk/web-audio
@@ -70,6 +159,67 @@ await recognizer.stop();
70
159
  await recognizer.destroy();
71
160
  ```
72
161
 
162
+ ## Session identifiers
163
+
164
+ `AudioRecognizer` creates one pseudonymous session identifier per application
165
+ and stores it in `localStorage` under
166
+ `sorisdk:web-audio:session:<appId>`. The value persists until the origin's
167
+ storage is cleared or the session manager's `clear()` method is called. An
168
+ upgrade does not rotate an existing non-empty value, including identifiers
169
+ created by older SDK versions.
170
+
171
+ New identifiers use `crypto.randomUUID()`. Browsers without `randomUUID()` use
172
+ `crypto.getRandomValues()` to create an RFC 4122 UUID v4. If neither secure API
173
+ is available, session creation fails instead of falling back to predictable
174
+ randomness. A host for such an environment must provide a cryptographically
175
+ secure generator explicitly:
176
+
177
+ ```ts
178
+ import {
179
+ AudioRecognizer,
180
+ createLocalStorageSessionManager
181
+ } from "@sorisdk/web-audio";
182
+
183
+ const sessionManager = createLocalStorageSessionManager({
184
+ key: "sorisdk:web-audio:session:YOUR_APP_ID",
185
+ generateSessionId: () => secureSessionIdFromYourRuntime()
186
+ });
187
+
188
+ const recognizer = new AudioRecognizer({
189
+ appId: "YOUR_APP_ID",
190
+ ephemeralKey: fetchEphemeralKeyFromYourServer,
191
+ sessionManager
192
+ });
193
+
194
+ // Stop recognition before intentionally rotating the identifier.
195
+ await recognizer.destroy();
196
+ await sessionManager.clear?.();
197
+ ```
198
+
199
+ Treat the identifier as persistent pseudonymous data: do not include personal
200
+ information in a custom value, and do not log or expose it unnecessarily.
201
+ Clearing it breaks device and activity continuity; the next authentication
202
+ creates a new server-side device identity.
203
+
204
+ ### SORI service contract
205
+
206
+ The current SORI service uses the identifier at these boundaries:
207
+
208
+ | Boundary | Use of `sessionId` | Security contract |
209
+ | --- | --- | --- |
210
+ | Authentication request | Debounces repeated requests and upserts an account-scoped device record | A valid application ID plus secret or ephemeral key is still required |
211
+ | Session token | Stored as the signed device claim | Possession of the raw identifier does not create or validate a token |
212
+ | Recognition activity | Attributes impressions, clicks, campaign links, and campaign webhooks to a device | Authorization comes from the signed token, not the identifier |
213
+ | Monitoring | Binds stored health and transition data to the token's device claim | A submitted device ID must match the signed claim |
214
+ | Authentication webhook | Populates the webhook `device_id` | The value is correlation data, not a webhook credential |
215
+ | Quota and throttling | Selects the authentication debounce bucket only | It is not a standalone billing or usage quota key |
216
+ | AudioPack caching | Not used as a cache partition | The browser cache remains application-scoped |
217
+ | Replay protection | No use | The stable identifier provides neither freshness nor replay protection |
218
+
219
+ Authorization, quota enforcement, cache isolation, and replay controls must
220
+ remain bound to authenticated server-side state rather than possession or
221
+ unpredictability of `sessionId`.
222
+
73
223
  ## Advanced wasm loading overrides
74
224
 
75
225
  If you need to pin explicit generated modules, use the nested `wasm` options:
Binary file
package/dist/index.js CHANGED
@@ -201,6 +201,7 @@ var WrappedFingerprintMatchWindow = class {
201
201
  constructor(inner) {
202
202
  this.inner = inner;
203
203
  }
204
+ inner;
204
205
  appendFingerprint(bytes) {
205
206
  const fn = getCallable(this.inner, ["appendFingerprint", "append_fingerprint"]);
206
207
  if (!fn) {
@@ -1025,11 +1026,31 @@ function resolveStorage2(storage) {
1025
1026
  }
1026
1027
  return globalThis.localStorage;
1027
1028
  }
1029
+ function formatUuidV4(bytes) {
1030
+ bytes[6] = bytes[6] & 15 | 64;
1031
+ bytes[8] = bytes[8] & 63 | 128;
1032
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1033
+ return [
1034
+ hex.slice(0, 8),
1035
+ hex.slice(8, 12),
1036
+ hex.slice(12, 16),
1037
+ hex.slice(16, 20),
1038
+ hex.slice(20)
1039
+ ].join("-");
1040
+ }
1028
1041
  function defaultGenerateSessionId() {
1029
- if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
1030
- return globalThis.crypto.randomUUID();
1042
+ const crypto = globalThis.crypto;
1043
+ if (typeof crypto !== "undefined") {
1044
+ if (typeof crypto.randomUUID === "function") {
1045
+ return crypto.randomUUID();
1046
+ }
1047
+ if (typeof crypto.getRandomValues === "function") {
1048
+ return formatUuidV4(crypto.getRandomValues(new Uint8Array(16)));
1049
+ }
1031
1050
  }
1032
- return `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1051
+ throw new Error(
1052
+ "Secure random number generation is unavailable; provide a cryptographically secure `generateSessionId`"
1053
+ );
1033
1054
  }
1034
1055
  function createLocalStorageSessionManager(options) {
1035
1056
  const storage = resolveStorage2(options.storage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sorisdk/web-audio",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Web SDK for browser-based audio recognition with SORI API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,17 +34,19 @@
34
34
  "license": "SEE LICENSE IN LICENSE.md",
35
35
  "homepage": "https://docs.soriapi.com/ko/integration/web",
36
36
  "dependencies": {
37
- "@sorisdk/matcher": "0.6.2",
38
- "@sorisdk/afpgen": "0.6.2"
37
+ "@sorisdk/matcher": "0.6.4",
38
+ "@sorisdk/afpgen": "0.6.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "tsup": "^8.5.1",
42
42
  "typescript": "^5.8.3",
43
- "vitest": "^3.2.4"
43
+ "vitest": "^3.2.6"
44
44
  },
45
45
  "scripts": {
46
46
  "build:wasm": "node ../../scripts/build-wasm-package.mjs web-audio",
47
47
  "build": "tsup src/index.ts --format esm --dts && pnpm run build:wasm && node ../../scripts/write-default-loader.mjs packages/web-audio ./generated/window.js",
48
+ "build:standalone": "pnpm --dir ../afpgen build && pnpm --dir ../matcher build && pnpm run build && vite build --config vite.standalone.config.ts && node ../../scripts/write-standalone-manifest.mjs && node ../../scripts/check-standalone-distribution.mjs",
49
+ "check:standalone": "node ../../scripts/check-standalone-distribution.mjs",
48
50
  "test": "vitest run",
49
51
  "test:watch": "vitest"
50
52
  }