@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 +151 -1
- package/dist/generated/window_bg.wasm +0 -0
- package/dist/index.js +24 -3
- package/package.json +6 -4
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
|
-
##
|
|
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
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
38
|
-
"@sorisdk/afpgen": "0.6.
|
|
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.
|
|
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
|
}
|