@octomil/browser 1.0.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.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +202 -0
- package/dist/cache.js.map +1 -0
- package/dist/device-auth.d.ts +41 -0
- package/dist/device-auth.d.ts.map +1 -0
- package/dist/device-auth.js +203 -0
- package/dist/device-auth.js.map +1 -0
- package/dist/experiments.d.ts +44 -0
- package/dist/experiments.d.ts.map +1 -0
- package/dist/experiments.js +135 -0
- package/dist/experiments.js.map +1 -0
- package/dist/federated.d.ts +53 -0
- package/dist/federated.d.ts.map +1 -0
- package/dist/federated.js +180 -0
- package/dist/federated.js.map +1 -0
- package/dist/index.cjs +2148 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/inference.d.ts +43 -0
- package/dist/inference.d.ts.map +1 -0
- package/dist/inference.js +213 -0
- package/dist/inference.js.map +1 -0
- package/dist/integrity.d.ts +19 -0
- package/dist/integrity.d.ts.map +1 -0
- package/dist/integrity.js +35 -0
- package/dist/integrity.js.map +1 -0
- package/dist/model-loader.d.ts +40 -0
- package/dist/model-loader.d.ts.map +1 -0
- package/dist/model-loader.js +232 -0
- package/dist/model-loader.js.map +1 -0
- package/dist/octomil.d.ts +92 -0
- package/dist/octomil.d.ts.map +1 -0
- package/dist/octomil.js +368 -0
- package/dist/octomil.js.map +1 -0
- package/dist/octomil.min.js +2849 -0
- package/dist/octomil.min.js.map +7 -0
- package/dist/privacy.d.ts +40 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +118 -0
- package/dist/privacy.js.map +1 -0
- package/dist/rollouts.d.ts +43 -0
- package/dist/rollouts.d.ts.map +1 -0
- package/dist/rollouts.js +114 -0
- package/dist/rollouts.js.map +1 -0
- package/dist/secure-aggregation.d.ts +50 -0
- package/dist/secure-aggregation.d.ts.map +1 -0
- package/dist/secure-aggregation.js +174 -0
- package/dist/secure-aggregation.js.map +1 -0
- package/dist/streaming.d.ts +25 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +148 -0
- package/dist/streaming.js.map +1 -0
- package/dist/telemetry.d.ts +41 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +130 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/types.d.ts +239 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OctoMil
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @octomil/browser
|
|
2
|
+
|
|
3
|
+
In-browser ML inference via ONNX Runtime Web + WebGPU.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@octomil/browser)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @octomil/browser
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { Octomil } from '@octomil/browser';
|
|
18
|
+
|
|
19
|
+
const ml = new Octomil({
|
|
20
|
+
model: 'https://models.octomil.com/sentiment-v1.onnx',
|
|
21
|
+
backend: 'webgpu',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await ml.load();
|
|
25
|
+
const result = await ml.predict({ text: 'This is amazing!' });
|
|
26
|
+
console.log(result.label, result.score);
|
|
27
|
+
ml.dispose();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Script Tag
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<script src="https://unpkg.com/@octomil/browser/dist/octomil.min.js"></script>
|
|
34
|
+
<script>
|
|
35
|
+
const ml = new Octomil({ model: 'model.onnx' });
|
|
36
|
+
ml.load().then(() => ml.predict({ text: 'hello' })).then(console.log);
|
|
37
|
+
</script>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `new Octomil(options)`
|
|
43
|
+
|
|
44
|
+
| Option | Type | Default | Description |
|
|
45
|
+
|--------|------|---------|-------------|
|
|
46
|
+
| `model` | `string` | required | Model URL or registry name |
|
|
47
|
+
| `backend` | `'webgpu' \| 'wasm'` | auto | Inference backend |
|
|
48
|
+
| `cacheStrategy` | `'cache-api' \| 'indexeddb' \| 'none'` | `'cache-api'` | Model caching |
|
|
49
|
+
| `serverUrl` | `string` | - | Server for registry resolution |
|
|
50
|
+
| `apiKey` | `string` | - | API key for authenticated downloads |
|
|
51
|
+
|
|
52
|
+
### Methods
|
|
53
|
+
|
|
54
|
+
| Method | Returns | Description |
|
|
55
|
+
|--------|---------|-------------|
|
|
56
|
+
| `load()` | `Promise<void>` | Download and initialize model |
|
|
57
|
+
| `predict(input)` | `Promise<PredictOutput>` | Run inference |
|
|
58
|
+
| `chat(messages)` | `Promise<ChatResponse>` | Chat completions |
|
|
59
|
+
| `dispose()` | `void` | Release resources |
|
|
60
|
+
|
|
61
|
+
### Input Formats
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
await ml.predict({ raw: new Float32Array([...]), dims: [1, 3, 224, 224] });
|
|
65
|
+
await ml.predict({ text: 'classify this' });
|
|
66
|
+
await ml.predict({ image: document.querySelector('canvas') });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
[docs.octomil.com](https://docs.octomil.com)
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — Model cache manager
|
|
3
|
+
*
|
|
4
|
+
* Caches downloaded ONNX model binaries using the Cache API (preferred)
|
|
5
|
+
* or IndexedDB as a fallback. Cache entries are keyed by model URL so
|
|
6
|
+
* that different model versions naturally invalidate stale entries.
|
|
7
|
+
*/
|
|
8
|
+
import type { CacheInfo, CacheStrategy } from "./types.js";
|
|
9
|
+
export interface ModelCache {
|
|
10
|
+
get(key: string): Promise<ArrayBuffer | null>;
|
|
11
|
+
put(key: string, data: ArrayBuffer): Promise<void>;
|
|
12
|
+
has(key: string): Promise<boolean>;
|
|
13
|
+
remove(key: string): Promise<void>;
|
|
14
|
+
info(key: string): Promise<CacheInfo>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a {@link ModelCache} instance for the given strategy.
|
|
18
|
+
*
|
|
19
|
+
* Falls back gracefully:
|
|
20
|
+
* - `"cache-api"` — uses Cache API if available, else IndexedDB, else no-op.
|
|
21
|
+
* - `"indexeddb"` — uses IndexedDB if available, else no-op.
|
|
22
|
+
* - `"none"` — always no-op.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createModelCache(strategy: CacheStrategy): ModelCache;
|
|
25
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAgB3D,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACvC;AA8LD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,UAAU,CA2BpE"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — Model cache manager
|
|
3
|
+
*
|
|
4
|
+
* Caches downloaded ONNX model binaries using the Cache API (preferred)
|
|
5
|
+
* or IndexedDB as a fallback. Cache entries are keyed by model URL so
|
|
6
|
+
* that different model versions naturally invalidate stale entries.
|
|
7
|
+
*/
|
|
8
|
+
import { OctomilError } from "./types.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Constants
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const CACHE_NAME = "octomil-models-v1";
|
|
13
|
+
const IDB_DB_NAME = "octomil-models";
|
|
14
|
+
const IDB_STORE_NAME = "blobs";
|
|
15
|
+
const IDB_VERSION = 1;
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Cache API implementation
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
class CacheApiModelCache {
|
|
20
|
+
async open() {
|
|
21
|
+
return caches.open(CACHE_NAME);
|
|
22
|
+
}
|
|
23
|
+
async get(key) {
|
|
24
|
+
const cache = await this.open();
|
|
25
|
+
const response = await cache.match(key);
|
|
26
|
+
if (!response)
|
|
27
|
+
return null;
|
|
28
|
+
return response.arrayBuffer();
|
|
29
|
+
}
|
|
30
|
+
async put(key, data) {
|
|
31
|
+
const cache = await this.open();
|
|
32
|
+
const response = new Response(data, {
|
|
33
|
+
headers: {
|
|
34
|
+
"x-octomil-cached-at": new Date().toISOString(),
|
|
35
|
+
"x-octomil-size": String(data.byteLength),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
await cache.put(key, response);
|
|
39
|
+
}
|
|
40
|
+
async has(key) {
|
|
41
|
+
const cache = await this.open();
|
|
42
|
+
const match = await cache.match(key);
|
|
43
|
+
return match !== undefined;
|
|
44
|
+
}
|
|
45
|
+
async remove(key) {
|
|
46
|
+
const cache = await this.open();
|
|
47
|
+
await cache.delete(key);
|
|
48
|
+
}
|
|
49
|
+
async info(key) {
|
|
50
|
+
const cache = await this.open();
|
|
51
|
+
const response = await cache.match(key);
|
|
52
|
+
if (!response) {
|
|
53
|
+
return { cached: false, sizeBytes: 0 };
|
|
54
|
+
}
|
|
55
|
+
const sizeHeader = response.headers.get("x-octomil-size");
|
|
56
|
+
const cachedAtHeader = response.headers.get("x-octomil-cached-at");
|
|
57
|
+
return {
|
|
58
|
+
cached: true,
|
|
59
|
+
sizeBytes: sizeHeader ? parseInt(sizeHeader, 10) : 0,
|
|
60
|
+
cachedAt: cachedAtHeader ?? undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
class IndexedDBModelCache {
|
|
65
|
+
dbPromise = null;
|
|
66
|
+
openDB() {
|
|
67
|
+
if (this.dbPromise)
|
|
68
|
+
return this.dbPromise;
|
|
69
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
70
|
+
const request = indexedDB.open(IDB_DB_NAME, IDB_VERSION);
|
|
71
|
+
request.onupgradeneeded = () => {
|
|
72
|
+
const db = request.result;
|
|
73
|
+
if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
|
|
74
|
+
db.createObjectStore(IDB_STORE_NAME, { keyPath: "key" });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
request.onsuccess = () => resolve(request.result);
|
|
78
|
+
request.onerror = () => reject(request.error);
|
|
79
|
+
});
|
|
80
|
+
return this.dbPromise;
|
|
81
|
+
}
|
|
82
|
+
async tx(mode) {
|
|
83
|
+
const db = await this.openDB();
|
|
84
|
+
const transaction = db.transaction(IDB_STORE_NAME, mode);
|
|
85
|
+
return transaction.objectStore(IDB_STORE_NAME);
|
|
86
|
+
}
|
|
87
|
+
async get(key) {
|
|
88
|
+
const store = await this.tx("readonly");
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const request = store.get(key);
|
|
91
|
+
request.onsuccess = () => {
|
|
92
|
+
const entry = request.result;
|
|
93
|
+
resolve(entry?.data ?? null);
|
|
94
|
+
};
|
|
95
|
+
request.onerror = () => reject(request.error);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async put(key, data) {
|
|
99
|
+
const store = await this.tx("readwrite");
|
|
100
|
+
const entry = {
|
|
101
|
+
key,
|
|
102
|
+
data,
|
|
103
|
+
sizeBytes: data.byteLength,
|
|
104
|
+
cachedAt: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const request = store.put(entry);
|
|
108
|
+
request.onsuccess = () => resolve();
|
|
109
|
+
request.onerror = () => reject(request.error);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async has(key) {
|
|
113
|
+
const store = await this.tx("readonly");
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const request = store.count(key);
|
|
116
|
+
request.onsuccess = () => resolve(request.result > 0);
|
|
117
|
+
request.onerror = () => reject(request.error);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async remove(key) {
|
|
121
|
+
const store = await this.tx("readwrite");
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const request = store.delete(key);
|
|
124
|
+
request.onsuccess = () => resolve();
|
|
125
|
+
request.onerror = () => reject(request.error);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async info(key) {
|
|
129
|
+
const store = await this.tx("readonly");
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const request = store.get(key);
|
|
132
|
+
request.onsuccess = () => {
|
|
133
|
+
const entry = request.result;
|
|
134
|
+
if (!entry) {
|
|
135
|
+
resolve({ cached: false, sizeBytes: 0 });
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
resolve({
|
|
139
|
+
cached: true,
|
|
140
|
+
sizeBytes: entry.sizeBytes,
|
|
141
|
+
cachedAt: entry.cachedAt,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
request.onerror = () => reject(request.error);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// No-op implementation
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
class NoopModelCache {
|
|
153
|
+
async get(_key) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
async put(_key, _data) {
|
|
157
|
+
/* no-op */
|
|
158
|
+
}
|
|
159
|
+
async has(_key) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
async remove(_key) {
|
|
163
|
+
/* no-op */
|
|
164
|
+
}
|
|
165
|
+
async info(_key) {
|
|
166
|
+
return { cached: false, sizeBytes: 0 };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Factory
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
/**
|
|
173
|
+
* Create a {@link ModelCache} instance for the given strategy.
|
|
174
|
+
*
|
|
175
|
+
* Falls back gracefully:
|
|
176
|
+
* - `"cache-api"` — uses Cache API if available, else IndexedDB, else no-op.
|
|
177
|
+
* - `"indexeddb"` — uses IndexedDB if available, else no-op.
|
|
178
|
+
* - `"none"` — always no-op.
|
|
179
|
+
*/
|
|
180
|
+
export function createModelCache(strategy) {
|
|
181
|
+
if (strategy === "none") {
|
|
182
|
+
return new NoopModelCache();
|
|
183
|
+
}
|
|
184
|
+
if (strategy === "cache-api") {
|
|
185
|
+
if (typeof caches !== "undefined") {
|
|
186
|
+
return new CacheApiModelCache();
|
|
187
|
+
}
|
|
188
|
+
// Fallback to IndexedDB when Cache API is unavailable.
|
|
189
|
+
if (typeof indexedDB !== "undefined") {
|
|
190
|
+
return new IndexedDBModelCache();
|
|
191
|
+
}
|
|
192
|
+
return new NoopModelCache();
|
|
193
|
+
}
|
|
194
|
+
if (strategy === "indexeddb") {
|
|
195
|
+
if (typeof indexedDB !== "undefined") {
|
|
196
|
+
return new IndexedDBModelCache();
|
|
197
|
+
}
|
|
198
|
+
return new NoopModelCache();
|
|
199
|
+
}
|
|
200
|
+
throw new OctomilError("CACHE_ERROR", `Unknown cache strategy: ${strategy}`);
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — Device authentication manager
|
|
3
|
+
*
|
|
4
|
+
* Handles device registration, token bootstrap/refresh/revoke for
|
|
5
|
+
* authenticated model downloads and training round participation.
|
|
6
|
+
*/
|
|
7
|
+
import type { DeviceAuthConfig } from "./types.js";
|
|
8
|
+
export declare class DeviceAuthManager {
|
|
9
|
+
private readonly serverUrl;
|
|
10
|
+
private readonly apiKey;
|
|
11
|
+
private token;
|
|
12
|
+
private deviceId;
|
|
13
|
+
private refreshTimer;
|
|
14
|
+
private disposed;
|
|
15
|
+
constructor(config: DeviceAuthConfig);
|
|
16
|
+
/** Register this device and obtain an initial auth token. */
|
|
17
|
+
bootstrap(orgId: string): Promise<void>;
|
|
18
|
+
/** Get a valid access token, refreshing if needed. */
|
|
19
|
+
getToken(): Promise<string>;
|
|
20
|
+
/** Refresh the current token. */
|
|
21
|
+
refreshToken(): Promise<void>;
|
|
22
|
+
/** Revoke the current token and clear local state. */
|
|
23
|
+
revokeToken(): Promise<void>;
|
|
24
|
+
/** Whether we currently hold a valid token. */
|
|
25
|
+
get isAuthenticated(): boolean;
|
|
26
|
+
/** Current device identifier. */
|
|
27
|
+
get currentDeviceId(): string | null;
|
|
28
|
+
/** Release timers. */
|
|
29
|
+
dispose(): void;
|
|
30
|
+
private request;
|
|
31
|
+
private isTokenExpired;
|
|
32
|
+
private isTokenExpiringSoon;
|
|
33
|
+
private scheduleRefresh;
|
|
34
|
+
private clearRefreshTimer;
|
|
35
|
+
private clearState;
|
|
36
|
+
/** Generate a stable device ID by hashing browser fingerprint data. */
|
|
37
|
+
generateDeviceId(): Promise<string>;
|
|
38
|
+
private collectDeviceInfo;
|
|
39
|
+
private ensureNotDisposed;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=device-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-auth.d.ts","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EACV,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAYpB,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,gBAAgB;IASpC,6DAA6D;IACvD,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsC7C,sDAAsD;IAChD,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAiBjC,iCAAiC;IAC3B,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA0CnC,sDAAsD;IAChD,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBlC,+CAA+C;IAC/C,IAAI,eAAe,IAAI,OAAO,CAE7B;IAED,iCAAiC;IACjC,IAAI,eAAe,IAAI,MAAM,GAAG,IAAI,CAEnC;IAED,sBAAsB;IACtB,OAAO,IAAI,IAAI;YAUD,OAAO;IAYrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,UAAU;IAKlB,uEAAuE;IACjE,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;IAkBzC,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,iBAAiB;CAQ1B"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — Device authentication manager
|
|
3
|
+
*
|
|
4
|
+
* Handles device registration, token bootstrap/refresh/revoke for
|
|
5
|
+
* authenticated model downloads and training round participation.
|
|
6
|
+
*/
|
|
7
|
+
import { OctomilError } from "./types.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Constants
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const REFRESH_BUFFER_MS = 30_000; // refresh 30s before expiry
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// DeviceAuthManager
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export class DeviceAuthManager {
|
|
16
|
+
serverUrl;
|
|
17
|
+
apiKey;
|
|
18
|
+
token = null;
|
|
19
|
+
deviceId = null;
|
|
20
|
+
refreshTimer = null;
|
|
21
|
+
disposed = false;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.serverUrl = config.serverUrl;
|
|
24
|
+
this.apiKey = config.apiKey;
|
|
25
|
+
}
|
|
26
|
+
// -----------------------------------------------------------------------
|
|
27
|
+
// Public
|
|
28
|
+
// -----------------------------------------------------------------------
|
|
29
|
+
/** Register this device and obtain an initial auth token. */
|
|
30
|
+
async bootstrap(orgId) {
|
|
31
|
+
this.ensureNotDisposed();
|
|
32
|
+
this.deviceId = await this.generateDeviceId();
|
|
33
|
+
const deviceInfo = this.collectDeviceInfo();
|
|
34
|
+
const response = await this.request("/api/v1/devices/register", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
org_id: orgId,
|
|
38
|
+
device_id: this.deviceId,
|
|
39
|
+
platform: "browser",
|
|
40
|
+
info: deviceInfo,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new OctomilError("NETWORK_ERROR", `Device registration failed: HTTP ${response.status}`);
|
|
45
|
+
}
|
|
46
|
+
const data = (await response.json());
|
|
47
|
+
this.token = {
|
|
48
|
+
accessToken: data.token,
|
|
49
|
+
refreshToken: data.refresh_token,
|
|
50
|
+
expiresAt: new Date(data.expires_at).getTime(),
|
|
51
|
+
};
|
|
52
|
+
this.scheduleRefresh();
|
|
53
|
+
}
|
|
54
|
+
/** Get a valid access token, refreshing if needed. */
|
|
55
|
+
async getToken() {
|
|
56
|
+
this.ensureNotDisposed();
|
|
57
|
+
if (!this.token) {
|
|
58
|
+
throw new OctomilError("NETWORK_ERROR", "Not authenticated. Call bootstrap() first.");
|
|
59
|
+
}
|
|
60
|
+
if (this.isTokenExpiringSoon()) {
|
|
61
|
+
await this.refreshToken();
|
|
62
|
+
}
|
|
63
|
+
return this.token.accessToken;
|
|
64
|
+
}
|
|
65
|
+
/** Refresh the current token. */
|
|
66
|
+
async refreshToken() {
|
|
67
|
+
this.ensureNotDisposed();
|
|
68
|
+
if (!this.token) {
|
|
69
|
+
throw new OctomilError("NETWORK_ERROR", "No token to refresh. Call bootstrap() first.");
|
|
70
|
+
}
|
|
71
|
+
const response = await this.request("/api/v1/auth/refresh", {
|
|
72
|
+
method: "POST",
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
refresh_token: this.token.refreshToken,
|
|
75
|
+
device_id: this.deviceId,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
// Token expired beyond repair — clear state
|
|
80
|
+
this.token = null;
|
|
81
|
+
throw new OctomilError("NETWORK_ERROR", `Token refresh failed: HTTP ${response.status}`);
|
|
82
|
+
}
|
|
83
|
+
const data = (await response.json());
|
|
84
|
+
this.token = {
|
|
85
|
+
accessToken: data.token,
|
|
86
|
+
refreshToken: data.refresh_token,
|
|
87
|
+
expiresAt: new Date(data.expires_at).getTime(),
|
|
88
|
+
};
|
|
89
|
+
this.scheduleRefresh();
|
|
90
|
+
}
|
|
91
|
+
/** Revoke the current token and clear local state. */
|
|
92
|
+
async revokeToken() {
|
|
93
|
+
this.ensureNotDisposed();
|
|
94
|
+
if (!this.token)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
await this.request("/api/v1/auth/revoke", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
token: this.token.accessToken,
|
|
101
|
+
device_id: this.deviceId,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
this.clearState();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Whether we currently hold a valid token. */
|
|
110
|
+
get isAuthenticated() {
|
|
111
|
+
return this.token !== null && !this.isTokenExpired();
|
|
112
|
+
}
|
|
113
|
+
/** Current device identifier. */
|
|
114
|
+
get currentDeviceId() {
|
|
115
|
+
return this.deviceId;
|
|
116
|
+
}
|
|
117
|
+
/** Release timers. */
|
|
118
|
+
dispose() {
|
|
119
|
+
if (this.disposed)
|
|
120
|
+
return;
|
|
121
|
+
this.disposed = true;
|
|
122
|
+
this.clearRefreshTimer();
|
|
123
|
+
}
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
// Internal
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
async request(path, init) {
|
|
128
|
+
const headers = {
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
131
|
+
};
|
|
132
|
+
return fetch(`${this.serverUrl}${path}`, {
|
|
133
|
+
...init,
|
|
134
|
+
headers: { ...headers, ...init.headers },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
isTokenExpired() {
|
|
138
|
+
if (!this.token)
|
|
139
|
+
return true;
|
|
140
|
+
return Date.now() >= this.token.expiresAt;
|
|
141
|
+
}
|
|
142
|
+
isTokenExpiringSoon() {
|
|
143
|
+
if (!this.token)
|
|
144
|
+
return true;
|
|
145
|
+
return Date.now() >= this.token.expiresAt - REFRESH_BUFFER_MS;
|
|
146
|
+
}
|
|
147
|
+
scheduleRefresh() {
|
|
148
|
+
this.clearRefreshTimer();
|
|
149
|
+
if (!this.token)
|
|
150
|
+
return;
|
|
151
|
+
const delay = Math.max(0, this.token.expiresAt - Date.now() - REFRESH_BUFFER_MS);
|
|
152
|
+
this.refreshTimer = setTimeout(() => {
|
|
153
|
+
void this.refreshToken().catch(() => {
|
|
154
|
+
// Best-effort auto-refresh; next getToken() call will retry.
|
|
155
|
+
});
|
|
156
|
+
}, delay);
|
|
157
|
+
}
|
|
158
|
+
clearRefreshTimer() {
|
|
159
|
+
if (this.refreshTimer !== null) {
|
|
160
|
+
clearTimeout(this.refreshTimer);
|
|
161
|
+
this.refreshTimer = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
clearState() {
|
|
165
|
+
this.token = null;
|
|
166
|
+
this.clearRefreshTimer();
|
|
167
|
+
}
|
|
168
|
+
/** Generate a stable device ID by hashing browser fingerprint data. */
|
|
169
|
+
async generateDeviceId() {
|
|
170
|
+
const raw = [
|
|
171
|
+
typeof navigator !== "undefined" ? navigator.userAgent : "unknown",
|
|
172
|
+
typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : "0x0",
|
|
173
|
+
typeof Intl !== "undefined"
|
|
174
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
175
|
+
: "UTC",
|
|
176
|
+
typeof navigator !== "undefined" ? navigator.language : "en",
|
|
177
|
+
].join("|");
|
|
178
|
+
const data = new TextEncoder().encode(raw);
|
|
179
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
180
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
181
|
+
return Array.from(hashArray)
|
|
182
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
183
|
+
.join("");
|
|
184
|
+
}
|
|
185
|
+
collectDeviceInfo() {
|
|
186
|
+
return {
|
|
187
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "unknown",
|
|
188
|
+
language: typeof navigator !== "undefined" ? navigator.language : "en",
|
|
189
|
+
screenWidth: typeof screen !== "undefined" ? screen.width : 0,
|
|
190
|
+
screenHeight: typeof screen !== "undefined" ? screen.height : 0,
|
|
191
|
+
timezone: typeof Intl !== "undefined"
|
|
192
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
193
|
+
: "UTC",
|
|
194
|
+
webgpu: typeof navigator !== "undefined" && "gpu" in navigator,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
ensureNotDisposed() {
|
|
198
|
+
if (this.disposed) {
|
|
199
|
+
throw new OctomilError("SESSION_DISPOSED", "DeviceAuthManager has been disposed.");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=device-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-auth.js","sourceRoot":"","sources":["../src/device-auth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO1C,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,MAAM,iBAAiB,GAAG,MAAM,CAAC,CAAC,4BAA4B;AAE9D,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,MAAM,OAAO,iBAAiB;IACX,SAAS,CAAS;IAClB,MAAM,CAAS;IACxB,KAAK,GAA2B,IAAI,CAAC;IACrC,QAAQ,GAAkB,IAAI,CAAC;IAC/B,YAAY,GAAyC,IAAI,CAAC;IAC1D,QAAQ,GAAG,KAAK,CAAC;IAEzB,YAAY,MAAwB;QAClC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QAClC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC9B,CAAC;IAED,0EAA0E;IAC1E,SAAS;IACT,0EAA0E;IAE1E,6DAA6D;IAC7D,KAAK,CAAC,SAAS,CAAC,KAAa;QAC3B,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,IAAI,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAE5C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,0BAA0B,EAAE;YAC9D,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,MAAM,EAAE,KAAK;gBACb,SAAS,EAAE,IAAI,CAAC,QAAQ;gBACxB,QAAQ,EAAE,SAAS;gBACnB,IAAI,EAAE,UAAU;aACjB,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,YAAY,CACpB,eAAe,EACf,oCAAoC,QAAQ,CAAC,MAAM,EAAE,CACtD,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAIlC,CAAC;QAEF,IAAI,CAAC,KAAK,GAAG;YACX,WAAW,EAAE,IAAI,CAAC,KAAK;YACvB,YAAY,EAAE,IAAI,CAAC,aAAa;YAChC,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;SAC/C,CAAC;QAEF,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,QAAQ;QACZ,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,YAAY,CACpB,eAAe,EACf,4CAA4C,CAC7C,CAAC;QACJ,CAAC;QAED,IAAI,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC5B,CAAC;QAED,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;IAChC,CAAC;IAED,iCAAiC;IACjC,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,YAAY,CACpB,eAAe,EACf,8CAA8C,CAC/C,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE;YAC1D,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY;gBACtC,SAAS,EAAE,IAAI,CAAC,QAAQ;aACzB,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,4CAA4C;YAC5C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,MAAM,IAAI,YAAY,CACpB,eAAe,EACf,8BAA8B,QAAQ,CAAC,MAAM,EAAE,CAChD,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAIlC,CAAC;QAEF,IAAI,CAAC,KAAK,GAAG;YACX,WAAW,EAAE,IAAI,CAAC,KAAK;YACvB,YAAY,EAAE,IAAI,CAAC,aAAa;YAChC,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;SAC/C,CAAC;QAEF,IAAI,CAAC,eAAe,EAAE,CAAC;IACzB,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,WAAW;QACf,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QAExB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE;gBACxC,MAAM,EAAE,MAAM;gBACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW;oBAC7B,SAAS,EAAE,IAAI,CAAC,QAAQ;iBACzB,CAAC;aACH,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,CAAC;IACH,CAAC;IAED,+CAA+C;IAC/C,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;IACvD,CAAC;IAED,iCAAiC;IACjC,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,sBAAsB;IACtB,OAAO;QACL,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,iBAAiB,EAAE,CAAC;IAC3B,CAAC;IAED,0EAA0E;IAC1E,WAAW;IACX,0EAA0E;IAElE,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAAiB;QACnD,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;SACvC,CAAC;QAEF,OAAO,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,EAAE,EAAE;YACvC,GAAG,IAAI;YACP,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,GAAI,IAAI,CAAC,OAAkC,EAAE;SACrE,CAAC,CAAC;IACL,CAAC;IAEO,cAAc;QACpB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAC7B,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;IAC5C,CAAC;IAEO,mBAAmB;QACzB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAC7B,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,iBAAiB,CAAC;IAChE,CAAC;IAEO,eAAe;QACrB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAEzB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB,CAAC,CAAC;QAEjF,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;YAClC,KAAK,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;gBAClC,6DAA6D;YAC/D,CAAC,CAAC,CAAC;QACL,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;IAEO,iBAAiB;QACvB,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YAC/B,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAChC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;IACH,CAAC;IAEO,UAAU;QAChB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,iBAAiB,EAAE,CAAC;IAC3B,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,gBAAgB;QACpB,MAAM,GAAG,GAAG;YACV,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;YAClE,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK;YAC1E,OAAO,IAAI,KAAK,WAAW;gBACzB,CAAC,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ;gBAClD,CAAC,CAAC,KAAK;YACT,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;SAC7D,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEZ,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3C,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC/D,MAAM,SAAS,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;QAC7C,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC;aACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;aAC3C,IAAI,CAAC,EAAE,CAAC,CAAC;IACd,CAAC;IAEO,iBAAiB;QACvB,OAAO;YACL,SAAS,EAAE,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;YAC7E,QAAQ,EAAE,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI;YACtE,WAAW,EAAE,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7D,YAAY,EAAE,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC/D,QAAQ,EACN,OAAO,IAAI,KAAK,WAAW;gBACzB,CAAC,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ;gBAClD,CAAC,CAAC,KAAK;YACX,MAAM,EAAE,OAAO,SAAS,KAAK,WAAW,IAAI,KAAK,IAAI,SAAS;SAC/D,CAAC;IACJ,CAAC;IAEO,iBAAiB;QACvB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,IAAI,YAAY,CACpB,kBAAkB,EAClB,sCAAsC,CACvC,CAAC;QACJ,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @octomil/browser — A/B testing and experiments client
|
|
3
|
+
*
|
|
4
|
+
* Deterministic variant assignment, experiment config caching,
|
|
5
|
+
* and metric reporting for model experiments.
|
|
6
|
+
*/
|
|
7
|
+
import type { Experiment, ExperimentVariant, TelemetryEvent } from "./types.js";
|
|
8
|
+
export declare class ExperimentsClient {
|
|
9
|
+
private readonly serverUrl;
|
|
10
|
+
private readonly apiKey?;
|
|
11
|
+
private readonly cacheTtlMs;
|
|
12
|
+
private readonly onTelemetry?;
|
|
13
|
+
private experimentsCache;
|
|
14
|
+
constructor(options: {
|
|
15
|
+
serverUrl: string;
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
cacheTtlMs?: number;
|
|
18
|
+
onTelemetry?: (event: TelemetryEvent) => void;
|
|
19
|
+
});
|
|
20
|
+
/** Fetch all active experiments (cached). */
|
|
21
|
+
getActiveExperiments(): Promise<Experiment[]>;
|
|
22
|
+
/** Get full experiment config by ID. */
|
|
23
|
+
getExperimentConfig(experimentId: string): Promise<Experiment>;
|
|
24
|
+
/**
|
|
25
|
+
* Deterministic variant assignment.
|
|
26
|
+
* Hash(deviceId + experimentId) → bucket → variant by cumulative traffic %.
|
|
27
|
+
*/
|
|
28
|
+
getVariant(experiment: Experiment, deviceId: string): ExperimentVariant | null;
|
|
29
|
+
/** Check if a device is enrolled in a specific experiment. */
|
|
30
|
+
isEnrolled(experiment: Experiment, deviceId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Find which experiment (if any) affects a given model, and return
|
|
33
|
+
* the variant this device should use.
|
|
34
|
+
*/
|
|
35
|
+
resolveModelExperiment(modelId: string, deviceId: string): Promise<{
|
|
36
|
+
experiment: Experiment;
|
|
37
|
+
variant: ExperimentVariant;
|
|
38
|
+
} | null>;
|
|
39
|
+
/** Report a metric for an experiment. */
|
|
40
|
+
trackMetric(experimentId: string, metricName: string, value: number, deviceId?: string): Promise<void>;
|
|
41
|
+
/** Clear the experiment cache. */
|
|
42
|
+
clearCache(): void;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=experiments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments.d.ts","sourceRoot":"","sources":["../src/experiments.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,YAAY,CAAC;AAOpB,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAkC;IAE/D,OAAO,CAAC,gBAAgB,CAGR;gBAEJ,OAAO,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;KAC/C;IAOD,6CAA6C;IACvC,oBAAoB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IA4BnD,wCAAwC;IAClC,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAgBpE;;;OAGG;IACH,UAAU,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAiB9E,8DAA8D;IAC9D,UAAU,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAI7D;;;OAGG;IACG,sBAAsB,CAC1B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,UAAU,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,iBAAiB,CAAA;KAAE,GAAG,IAAI,CAAC;IAczE,yCAAyC;IACnC,WAAW,CACf,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IA0BhB,kCAAkC;IAClC,UAAU,IAAI,IAAI;CAGnB"}
|