@neta-art/cohub 2.3.0 → 2.5.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/dist/chunks/http.d.ts +178 -51
- package/dist/chunks/http.js +14 -0
- package/dist/http.d.ts +1 -1
- package/dist/index.d.ts +112 -52
- package/dist/index.js +636 -53
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -97,10 +97,10 @@ var UsersApi = class {
|
|
|
97
97
|
};
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region src/work-runtime.ts
|
|
100
|
-
const isBrowser = () => typeof window !== "undefined" && typeof window.parent !== "undefined";
|
|
101
|
-
const hasParent = () => isBrowser() && window.parent !== window;
|
|
100
|
+
const isBrowser$1 = () => typeof window !== "undefined" && typeof window.parent !== "undefined";
|
|
101
|
+
const hasParent = () => isBrowser$1() && window.parent !== window;
|
|
102
102
|
const getParentOrigin = () => {
|
|
103
|
-
if (!isBrowser()) return null;
|
|
103
|
+
if (!isBrowser$1()) return null;
|
|
104
104
|
const ancestorOrigin = window.location.ancestorOrigins?.[0];
|
|
105
105
|
if (typeof ancestorOrigin === "string" && ancestorOrigin) return ancestorOrigin;
|
|
106
106
|
try {
|
|
@@ -109,84 +109,258 @@ const getParentOrigin = () => {
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
const generateRequestId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
|
113
|
+
/**
|
|
114
|
+
* Bridge-mode transport: posts messages to `window.parent` (the Cohub host
|
|
115
|
+
* embedding the work in an iframe) and listens for the matching reply.
|
|
116
|
+
* Behaviorally identical to the previous module-level `request()` helper.
|
|
117
|
+
*/
|
|
118
|
+
var ParentBridgeTransport = class {
|
|
119
|
+
trustedParentOrigin = null;
|
|
120
|
+
request(message, options) {
|
|
121
|
+
const timeoutMs = options?.timeoutMs ?? 1200;
|
|
122
|
+
const retryIntervalMs = options?.retryIntervalMs;
|
|
123
|
+
if (!hasParent()) return Promise.resolve(null);
|
|
124
|
+
const requestId = generateRequestId();
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
let retryTimer = null;
|
|
127
|
+
const parentOrigin = this.trustedParentOrigin ?? getParentOrigin();
|
|
128
|
+
const postRequest = () => {
|
|
129
|
+
try {
|
|
130
|
+
window.parent.postMessage({
|
|
131
|
+
...message,
|
|
132
|
+
requestId
|
|
133
|
+
}, parentOrigin ?? "*");
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const cleanup = () => {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
if (retryTimer) clearInterval(retryTimer);
|
|
141
|
+
window.removeEventListener("message", onMessage);
|
|
142
|
+
};
|
|
143
|
+
const timer = setTimeout(() => {
|
|
144
|
+
cleanup();
|
|
145
|
+
resolve(null);
|
|
146
|
+
}, timeoutMs);
|
|
147
|
+
const onMessage = (event) => {
|
|
148
|
+
if (event.source !== window.parent) return;
|
|
149
|
+
if (parentOrigin && event.origin !== parentOrigin) return;
|
|
150
|
+
const data = event.data;
|
|
151
|
+
if (!data || data.requestId !== requestId) return;
|
|
152
|
+
cleanup();
|
|
153
|
+
this.trustedParentOrigin = event.origin;
|
|
154
|
+
if (data.type === "cohub.work.error") {
|
|
155
|
+
reject(new Error(data.message));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
resolve(data);
|
|
159
|
+
};
|
|
160
|
+
window.addEventListener("message", onMessage);
|
|
161
|
+
postRequest();
|
|
162
|
+
if (retryIntervalMs) retryTimer = setInterval(postRequest, retryIntervalMs);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Broker-mode transport for standalone-deployed works. Opens a popup window to
|
|
168
|
+
* the Cohub auth broker page, performs a ready-handshake, sends the request via
|
|
169
|
+
* postMessage, and resolves with the broker's response. The popup is closed
|
|
170
|
+
* after a single request is fulfilled (one-shot, per §7.2 of the plan).
|
|
171
|
+
*
|
|
172
|
+
* Non-interactive messages (`context`, `checkout-state`) are answered locally
|
|
173
|
+
* without opening a popup — the work already knows its own workId, and
|
|
174
|
+
* checkout state is not available on the work's own origin in broker mode.
|
|
175
|
+
*/
|
|
176
|
+
var PopupBrokerTransport = class {
|
|
177
|
+
brokerOrigin;
|
|
178
|
+
workId;
|
|
179
|
+
constructor(config) {
|
|
180
|
+
this.brokerOrigin = config.brokerOrigin;
|
|
181
|
+
this.workId = config.workId;
|
|
182
|
+
}
|
|
183
|
+
request(message, options) {
|
|
184
|
+
if (message.type === "cohub.work.context") return Promise.resolve({
|
|
185
|
+
type: "cohub.work.context.result",
|
|
186
|
+
context: {
|
|
187
|
+
work: {
|
|
188
|
+
id: this.workId,
|
|
189
|
+
slug: "",
|
|
190
|
+
url: null
|
|
191
|
+
},
|
|
192
|
+
space: { id: "" },
|
|
193
|
+
permissions: {
|
|
194
|
+
scopes: [],
|
|
195
|
+
workScopes: [],
|
|
196
|
+
viewerScopes: []
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
if (message.type === "cohub.work.checkout-state") return Promise.resolve({
|
|
201
|
+
type: "cohub.work.checkout-state.result",
|
|
202
|
+
status: null,
|
|
203
|
+
orderId: null
|
|
204
|
+
});
|
|
205
|
+
const timeoutMs = options?.timeoutMs ?? 12e4;
|
|
206
|
+
const requestId = generateRequestId();
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
if (typeof window === "undefined" || typeof window.open !== "function") {
|
|
209
|
+
resolve(null);
|
|
126
210
|
return;
|
|
127
211
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
};
|
|
134
|
-
const timer = setTimeout(() => {
|
|
135
|
-
cleanup();
|
|
136
|
-
resolve(null);
|
|
137
|
-
}, timeoutMs);
|
|
138
|
-
const onMessage = (event) => {
|
|
139
|
-
if (event.source !== window.parent) return;
|
|
140
|
-
if (parentOrigin && event.origin !== parentOrigin) return;
|
|
141
|
-
const data = event.data;
|
|
142
|
-
if (!data || data.requestId !== requestId) return;
|
|
143
|
-
cleanup();
|
|
144
|
-
trustedParentOrigin = event.origin;
|
|
145
|
-
if (data.type === "cohub.work.error") {
|
|
146
|
-
reject(new Error(data.message));
|
|
212
|
+
const workOrigin = window.location.origin;
|
|
213
|
+
const brokerUrl = `${this.brokerOrigin}/work-auth?work=${encodeURIComponent(this.workId)}&origin=${encodeURIComponent(workOrigin)}`;
|
|
214
|
+
const popup = window.open(brokerUrl, "cohub-work-auth", "popup,width=480,height=640");
|
|
215
|
+
if (!popup) {
|
|
216
|
+
reject(/* @__PURE__ */ new Error("Failed to open authorization window. Please allow popups for this site."));
|
|
147
217
|
return;
|
|
148
218
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
219
|
+
let ready = false;
|
|
220
|
+
let settled = false;
|
|
221
|
+
let timer = null;
|
|
222
|
+
let closeChecker = null;
|
|
223
|
+
const finish = (fn) => {
|
|
224
|
+
if (settled) return;
|
|
225
|
+
settled = true;
|
|
226
|
+
if (timer) clearTimeout(timer);
|
|
227
|
+
if (closeChecker) clearInterval(closeChecker);
|
|
228
|
+
window.removeEventListener("message", onMessage);
|
|
229
|
+
try {
|
|
230
|
+
popup.close();
|
|
231
|
+
} catch {}
|
|
232
|
+
fn();
|
|
233
|
+
};
|
|
234
|
+
timer = setTimeout(() => {
|
|
235
|
+
finish(() => {
|
|
236
|
+
if (!ready) reject(/* @__PURE__ */ new Error("Authorization window did not respond in time."));
|
|
237
|
+
else resolve(null);
|
|
238
|
+
});
|
|
239
|
+
}, timeoutMs);
|
|
240
|
+
const onMessage = (event) => {
|
|
241
|
+
if (event.source !== popup) return;
|
|
242
|
+
if (event.origin !== this.brokerOrigin) return;
|
|
243
|
+
const data = event.data;
|
|
244
|
+
if (!data) return;
|
|
245
|
+
if (data.type === "cohub.work.broker.ready" && !ready) {
|
|
246
|
+
ready = true;
|
|
247
|
+
try {
|
|
248
|
+
popup.postMessage({
|
|
249
|
+
...message,
|
|
250
|
+
requestId
|
|
251
|
+
}, this.brokerOrigin);
|
|
252
|
+
} catch {
|
|
253
|
+
finish(() => reject(/* @__PURE__ */ new Error("Failed to send request to authorization window.")));
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (data.requestId !== requestId) return;
|
|
258
|
+
finish(() => {
|
|
259
|
+
if (data.type === "cohub.work.error") {
|
|
260
|
+
reject(new Error(data.message));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
resolve(data);
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
window.addEventListener("message", onMessage);
|
|
267
|
+
closeChecker = setInterval(() => {
|
|
268
|
+
if (popup.closed) finish(() => {
|
|
269
|
+
if (!ready) reject(/* @__PURE__ */ new Error("Authorization window was closed."));
|
|
270
|
+
else resolve(null);
|
|
271
|
+
});
|
|
272
|
+
}, 500);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
155
275
|
};
|
|
276
|
+
const TOKEN_STORAGE_PREFIX = "cohub:work-token";
|
|
156
277
|
var WorkRuntimeApi = class {
|
|
157
278
|
token = null;
|
|
279
|
+
transport;
|
|
280
|
+
tokenStorageKey;
|
|
281
|
+
constructor(transport = new ParentBridgeTransport(), workId) {
|
|
282
|
+
this.transport = transport;
|
|
283
|
+
this.tokenStorageKey = workId ? `${TOKEN_STORAGE_PREFIX}:${workId}` : null;
|
|
284
|
+
this.token = this.readStoredToken();
|
|
285
|
+
}
|
|
286
|
+
readStoredToken() {
|
|
287
|
+
if (!this.tokenStorageKey || typeof localStorage === "undefined") return null;
|
|
288
|
+
try {
|
|
289
|
+
return localStorage.getItem(this.tokenStorageKey);
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
writeStoredToken(token) {
|
|
295
|
+
if (!this.tokenStorageKey || typeof localStorage === "undefined") return;
|
|
296
|
+
try {
|
|
297
|
+
if (token) localStorage.setItem(this.tokenStorageKey, token);
|
|
298
|
+
else localStorage.removeItem(this.tokenStorageKey);
|
|
299
|
+
} catch {}
|
|
300
|
+
}
|
|
158
301
|
async context() {
|
|
159
|
-
return (await request({ type: "cohub.work.context" },
|
|
302
|
+
return (await this.transport.request({ type: "cohub.work.context" }, {
|
|
303
|
+
timeoutMs: 8e3,
|
|
304
|
+
retryIntervalMs: 250
|
|
305
|
+
}))?.context ?? null;
|
|
160
306
|
}
|
|
161
307
|
async getAccessToken(options) {
|
|
162
308
|
if (this.token && !options?.forceRefresh) return this.token;
|
|
163
|
-
|
|
309
|
+
if (options?.forceRefresh) {
|
|
310
|
+
this.token = null;
|
|
311
|
+
this.writeStoredToken(null);
|
|
312
|
+
}
|
|
313
|
+
const response = await this.transport.request({
|
|
164
314
|
type: "cohub.work.token",
|
|
165
315
|
forceRefresh: Boolean(options?.forceRefresh)
|
|
166
|
-
}, 2e4);
|
|
316
|
+
}, { timeoutMs: 2e4 });
|
|
167
317
|
this.token = response?.token ?? null;
|
|
318
|
+
this.writeStoredToken(this.token);
|
|
168
319
|
return this.token;
|
|
169
320
|
}
|
|
170
321
|
async requestAuthorization(input) {
|
|
171
|
-
const response = await request({
|
|
322
|
+
const response = await this.transport.request({
|
|
172
323
|
type: "cohub.work.authorize",
|
|
173
324
|
scopes: input.scopes,
|
|
174
325
|
reason: input.reason
|
|
175
|
-
}, 12e4);
|
|
326
|
+
}, { timeoutMs: 12e4 });
|
|
176
327
|
this.token = response?.token ?? null;
|
|
328
|
+
this.writeStoredToken(this.token);
|
|
177
329
|
return Boolean(this.token);
|
|
178
330
|
}
|
|
179
331
|
async purchase(input) {
|
|
180
|
-
return (await request({
|
|
332
|
+
return (await this.transport.request({
|
|
181
333
|
type: "cohub.work.purchase",
|
|
182
334
|
productKey: input.productKey
|
|
183
|
-
}, 12e4))?.checkout ?? null;
|
|
335
|
+
}, { timeoutMs: 12e4 }))?.checkout ?? null;
|
|
184
336
|
}
|
|
185
337
|
async checkoutState() {
|
|
186
|
-
return await request({ type: "cohub.work.checkout-state" },
|
|
338
|
+
return await this.transport.request({ type: "cohub.work.checkout-state" }, {
|
|
339
|
+
timeoutMs: 8e3,
|
|
340
|
+
retryIntervalMs: 250
|
|
341
|
+
}) ?? null;
|
|
187
342
|
}
|
|
188
343
|
};
|
|
189
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Resolves the appropriate transport based on the work mode configuration.
|
|
346
|
+
* Auto-detection: inside an iframe → bridge; standalone with broker config →
|
|
347
|
+
* broker; otherwise → bridge (returns null for non-work contexts).
|
|
348
|
+
*/
|
|
349
|
+
function resolveWorkTransport(config) {
|
|
350
|
+
const explicitMode = config?.mode;
|
|
351
|
+
const brokerOrigin = config?.brokerOrigin;
|
|
352
|
+
const workId = config?.workId;
|
|
353
|
+
const hasBrokerConfig = Boolean(brokerOrigin && workId);
|
|
354
|
+
const createBroker = () => brokerOrigin && workId ? new PopupBrokerTransport({
|
|
355
|
+
brokerOrigin,
|
|
356
|
+
workId
|
|
357
|
+
}) : new ParentBridgeTransport();
|
|
358
|
+
if (explicitMode === "bridge") return new ParentBridgeTransport();
|
|
359
|
+
if (explicitMode === "broker") return createBroker();
|
|
360
|
+
if (typeof window !== "undefined" && window.parent !== window) return new ParentBridgeTransport();
|
|
361
|
+
return hasBrokerConfig ? createBroker() : new ParentBridgeTransport();
|
|
362
|
+
}
|
|
363
|
+
const createWorkRuntime = (transport, workId) => new WorkRuntimeApi(transport, workId);
|
|
190
364
|
//#endregion
|
|
191
365
|
//#region src/client.ts
|
|
192
366
|
var CohubClient = class {
|
|
@@ -214,7 +388,8 @@ var CohubClient = class {
|
|
|
214
388
|
workRuntime;
|
|
215
389
|
constructor(options = {}) {
|
|
216
390
|
const apiBaseUrl = resolveApiBaseUrl(options);
|
|
217
|
-
|
|
391
|
+
const workTransport = resolveWorkTransport(options.work);
|
|
392
|
+
this.workRuntime = createWorkRuntime(workTransport, options.work?.workId);
|
|
218
393
|
const getAccessToken = options.getAccessToken ?? ((tokenOptions) => this.workRuntime.getAccessToken(tokenOptions));
|
|
219
394
|
const resolvedOptions = {
|
|
220
395
|
...options,
|
|
@@ -340,6 +515,414 @@ var CohubClient = class {
|
|
|
340
515
|
};
|
|
341
516
|
const createCohubClient = (options) => new CohubClient(options);
|
|
342
517
|
//#endregion
|
|
518
|
+
//#region src/work-grant-cache.ts
|
|
519
|
+
const STORAGE_PREFIX = "cohub:work-grants";
|
|
520
|
+
const CACHE_VERSION = 1;
|
|
521
|
+
const MAX_AGE_MS = 336 * 60 * 60 * 1e3;
|
|
522
|
+
function isBrowser() {
|
|
523
|
+
return typeof localStorage !== "undefined";
|
|
524
|
+
}
|
|
525
|
+
function storageKey(userUuid, workId) {
|
|
526
|
+
return `${STORAGE_PREFIX}:${encodeURIComponent(userUuid)}:${encodeURIComponent(workId)}:v${CACHE_VERSION}`;
|
|
527
|
+
}
|
|
528
|
+
function userPrefix(userUuid) {
|
|
529
|
+
return `${STORAGE_PREFIX}:${encodeURIComponent(userUuid)}:`;
|
|
530
|
+
}
|
|
531
|
+
function isPermissionArray(value) {
|
|
532
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
533
|
+
}
|
|
534
|
+
function isCachedWorkGrant(value) {
|
|
535
|
+
if (!value || typeof value !== "object") return false;
|
|
536
|
+
const record = value;
|
|
537
|
+
return record.version === CACHE_VERSION && typeof record.userUuid === "string" && typeof record.workId === "string" && isPermissionArray(record.scopes) && typeof record.updatedAt === "number";
|
|
538
|
+
}
|
|
539
|
+
function readEntry(userUuid, workId) {
|
|
540
|
+
if (!isBrowser()) return null;
|
|
541
|
+
const key = storageKey(userUuid, workId);
|
|
542
|
+
try {
|
|
543
|
+
const raw = localStorage.getItem(key);
|
|
544
|
+
if (!raw) return null;
|
|
545
|
+
const parsed = JSON.parse(raw);
|
|
546
|
+
if (!isCachedWorkGrant(parsed) || parsed.userUuid !== userUuid || parsed.workId !== workId) {
|
|
547
|
+
localStorage.removeItem(key);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
if (Date.now() - parsed.updatedAt > MAX_AGE_MS) {
|
|
551
|
+
localStorage.removeItem(key);
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
return parsed;
|
|
555
|
+
} catch {
|
|
556
|
+
try {
|
|
557
|
+
localStorage.removeItem(key);
|
|
558
|
+
} catch {}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Returns true when the viewer has previously granted every requested scope
|
|
564
|
+
* for this work, allowing a silent re-authorization.
|
|
565
|
+
*/
|
|
566
|
+
function hasGrantedWorkScopes(userUuid, workId, scopes) {
|
|
567
|
+
if (!userUuid || !workId || scopes.length === 0) return false;
|
|
568
|
+
const entry = readEntry(userUuid, workId);
|
|
569
|
+
if (!entry) return false;
|
|
570
|
+
const granted = new Set(entry.scopes);
|
|
571
|
+
return scopes.every((scope) => granted.has(scope));
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Records the granted scopes for a work, merged with any previously granted
|
|
575
|
+
* scopes so a growing permission set stays covered.
|
|
576
|
+
*/
|
|
577
|
+
function setGrantedWorkScopes(userUuid, workId, scopes) {
|
|
578
|
+
if (!userUuid || !workId || scopes.length === 0) return;
|
|
579
|
+
const existing = readEntry(userUuid, workId);
|
|
580
|
+
const entry = {
|
|
581
|
+
version: CACHE_VERSION,
|
|
582
|
+
userUuid,
|
|
583
|
+
workId,
|
|
584
|
+
scopes: Array.from(new Set([...existing?.scopes ?? [], ...scopes])),
|
|
585
|
+
updatedAt: Date.now()
|
|
586
|
+
};
|
|
587
|
+
if (!isBrowser()) return;
|
|
588
|
+
try {
|
|
589
|
+
localStorage.setItem(storageKey(userUuid, workId), JSON.stringify(entry));
|
|
590
|
+
} catch {}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Clears cached grants. Pass a workId to clear a single work, or omit it to
|
|
594
|
+
* clear every cached grant for the user (used on sign-out).
|
|
595
|
+
*/
|
|
596
|
+
function clearGrantedWorkScopes(userUuid, workId) {
|
|
597
|
+
if (!isBrowser() || !userUuid) return;
|
|
598
|
+
try {
|
|
599
|
+
if (workId) {
|
|
600
|
+
localStorage.removeItem(storageKey(userUuid, workId));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const prefix = userPrefix(userUuid);
|
|
604
|
+
for (let i = localStorage.length - 1; i >= 0; i -= 1) {
|
|
605
|
+
const key = localStorage.key(i);
|
|
606
|
+
if (key?.startsWith(prefix)) localStorage.removeItem(key);
|
|
607
|
+
}
|
|
608
|
+
} catch {}
|
|
609
|
+
}
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/work-bridge-core.ts
|
|
612
|
+
function readTokenResponse(value) {
|
|
613
|
+
if (!value || typeof value !== "object") return null;
|
|
614
|
+
const token = value.token;
|
|
615
|
+
return typeof token === "string" && token ? token : null;
|
|
616
|
+
}
|
|
617
|
+
function clonePermissionScopes(scopes) {
|
|
618
|
+
return Array.from(scopes ?? []).filter((scope) => typeof scope === "string");
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Framework-agnostic work bridge host core — message handling, work session
|
|
622
|
+
* token minting, authorization (with silent re-grant cache), and
|
|
623
|
+
* purchase/checkout flow — without any rendering or reactive primitives.
|
|
624
|
+
*
|
|
625
|
+
* Both the Cohub iframe host (WorkSurface, Svelte) and the standalone broker
|
|
626
|
+
* page compose this with their own transport-specific reply and auth
|
|
627
|
+
* dependencies. External hosts (e.g. Neta-Studio in React) can do the same.
|
|
628
|
+
*/
|
|
629
|
+
function createWorkBridgeCore(config) {
|
|
630
|
+
const { work, reply, getCheckoutState, getAccessToken, getViewerUuid } = config;
|
|
631
|
+
const apiOrigin = config.apiOrigin;
|
|
632
|
+
const isBackground = config.isBackground ?? false;
|
|
633
|
+
const onStateChange = config.onStateChange;
|
|
634
|
+
let workToken = null;
|
|
635
|
+
const state = {
|
|
636
|
+
authOpen: false,
|
|
637
|
+
pendingAuth: null,
|
|
638
|
+
authError: null,
|
|
639
|
+
authSaving: false,
|
|
640
|
+
purchaseOpen: false,
|
|
641
|
+
pendingPurchase: null,
|
|
642
|
+
purchaseError: null,
|
|
643
|
+
purchaseSaving: false
|
|
644
|
+
};
|
|
645
|
+
function notify() {
|
|
646
|
+
onStateChange?.({ ...state });
|
|
647
|
+
}
|
|
648
|
+
const pendingPurchaseStorageKey = `cohub-work-purchase:${work.id}`;
|
|
649
|
+
async function isCurrentViewerWorkOwner() {
|
|
650
|
+
const viewerUuid = await getViewerUuid();
|
|
651
|
+
return Boolean(viewerUuid && viewerUuid === work.userUuid);
|
|
652
|
+
}
|
|
653
|
+
async function ensureBaseToken(forceRefresh = false) {
|
|
654
|
+
if (workToken && !forceRefresh) return workToken;
|
|
655
|
+
const userToken = await getAccessToken({ forceRefresh });
|
|
656
|
+
if (!userToken) {
|
|
657
|
+
await config.requestSignIn(typeof location !== "undefined" ? location.pathname : "/");
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
const response = await fetch(`${apiOrigin}/api/works/${work.id}/session`, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
headers: { Authorization: `Bearer ${userToken}` }
|
|
663
|
+
});
|
|
664
|
+
if (!response.ok) throw new Error("Failed to create work session.");
|
|
665
|
+
const token = readTokenResponse(await response.json());
|
|
666
|
+
if (!token) throw new Error("Invalid work session response.");
|
|
667
|
+
workToken = token;
|
|
668
|
+
return workToken;
|
|
669
|
+
}
|
|
670
|
+
async function authorize(scopes) {
|
|
671
|
+
const userToken = await getAccessToken();
|
|
672
|
+
if (!userToken) {
|
|
673
|
+
await config.requestSignIn(typeof location !== "undefined" ? location.pathname : "/");
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
const response = await fetch(`${apiOrigin}/api/works/${work.id}/authorize`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: {
|
|
679
|
+
Authorization: `Bearer ${userToken}`,
|
|
680
|
+
"Content-Type": "application/json"
|
|
681
|
+
},
|
|
682
|
+
body: JSON.stringify({ scopes })
|
|
683
|
+
});
|
|
684
|
+
if (!response.ok) throw new Error((await response.json().catch(() => null))?.message ?? "Authorization failed.");
|
|
685
|
+
const token = readTokenResponse(await response.json());
|
|
686
|
+
if (!token) throw new Error("Invalid work authorization response.");
|
|
687
|
+
workToken = token;
|
|
688
|
+
return workToken;
|
|
689
|
+
}
|
|
690
|
+
function writePendingPurchase(input) {
|
|
691
|
+
if (typeof sessionStorage === "undefined") return;
|
|
692
|
+
try {
|
|
693
|
+
sessionStorage.setItem(pendingPurchaseStorageKey, JSON.stringify({
|
|
694
|
+
...input,
|
|
695
|
+
at: Date.now()
|
|
696
|
+
}));
|
|
697
|
+
} catch {}
|
|
698
|
+
}
|
|
699
|
+
function readPendingPurchase() {
|
|
700
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
701
|
+
try {
|
|
702
|
+
const raw = sessionStorage.getItem(pendingPurchaseStorageKey);
|
|
703
|
+
if (!raw) return null;
|
|
704
|
+
const parsed = JSON.parse(raw);
|
|
705
|
+
return typeof parsed.orderId === "string" && typeof parsed.productKey === "string" && typeof parsed.at === "number" ? {
|
|
706
|
+
orderId: parsed.orderId,
|
|
707
|
+
productKey: parsed.productKey,
|
|
708
|
+
at: parsed.at
|
|
709
|
+
} : null;
|
|
710
|
+
} catch {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function clearPendingPurchase() {
|
|
715
|
+
if (typeof sessionStorage === "undefined") return;
|
|
716
|
+
try {
|
|
717
|
+
sessionStorage.removeItem(pendingPurchaseStorageKey);
|
|
718
|
+
} catch {}
|
|
719
|
+
}
|
|
720
|
+
async function createPurchase(productKey) {
|
|
721
|
+
const userToken = await getAccessToken();
|
|
722
|
+
if (!userToken) {
|
|
723
|
+
await config.requestSignIn(typeof location !== "undefined" ? location.pathname + location.search + location.hash : "/");
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const response = await fetch(`${apiOrigin}/api/works/${work.id}/commerce/purchase`, {
|
|
727
|
+
method: "POST",
|
|
728
|
+
headers: {
|
|
729
|
+
Authorization: `Bearer ${userToken}`,
|
|
730
|
+
"Content-Type": "application/json"
|
|
731
|
+
},
|
|
732
|
+
body: JSON.stringify({ productKey })
|
|
733
|
+
});
|
|
734
|
+
if (!response.ok) throw new Error((await response.json().catch(() => null))?.message ?? "Purchase failed.");
|
|
735
|
+
return (await response.json()).checkout ?? null;
|
|
736
|
+
}
|
|
737
|
+
async function handleMessage(event) {
|
|
738
|
+
const data = event.data;
|
|
739
|
+
if (!data?.requestId) return;
|
|
740
|
+
try {
|
|
741
|
+
if (data.type === "cohub.work.context") {
|
|
742
|
+
const workScopes = clonePermissionScopes(work.workScopes);
|
|
743
|
+
reply(data.requestId, {
|
|
744
|
+
type: "cohub.work.context.result",
|
|
745
|
+
context: {
|
|
746
|
+
work: {
|
|
747
|
+
id: work.id,
|
|
748
|
+
slug: work.slug,
|
|
749
|
+
url: typeof location !== "undefined" ? location.href : ""
|
|
750
|
+
},
|
|
751
|
+
space: { id: work.spaceId },
|
|
752
|
+
permissions: {
|
|
753
|
+
scopes: workScopes,
|
|
754
|
+
workScopes,
|
|
755
|
+
viewerScopes: []
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (data.type === "cohub.work.token") {
|
|
761
|
+
const token = await ensureBaseToken(Boolean(data.forceRefresh));
|
|
762
|
+
reply(data.requestId, {
|
|
763
|
+
type: "cohub.work.token.result",
|
|
764
|
+
token
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
if (data.type === "cohub.work.checkout-state") {
|
|
768
|
+
const pending = readPendingPurchase();
|
|
769
|
+
const checkoutState = getCheckoutState();
|
|
770
|
+
const orderId = checkoutState.orderId ?? pending?.orderId ?? null;
|
|
771
|
+
if (checkoutState.status && checkoutState.orderId) clearPendingPurchase();
|
|
772
|
+
reply(data.requestId, {
|
|
773
|
+
type: "cohub.work.checkout-state.result",
|
|
774
|
+
status: checkoutState.status,
|
|
775
|
+
orderId
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (data.type === "cohub.work.purchase") {
|
|
779
|
+
const productKey = typeof data.productKey === "string" ? data.productKey.trim() : "";
|
|
780
|
+
if (!productKey) {
|
|
781
|
+
reply(data.requestId, {
|
|
782
|
+
type: "cohub.work.error",
|
|
783
|
+
message: "Product key is required."
|
|
784
|
+
});
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
state.pendingPurchase = {
|
|
788
|
+
requestId: data.requestId,
|
|
789
|
+
productKey
|
|
790
|
+
};
|
|
791
|
+
state.purchaseError = null;
|
|
792
|
+
state.purchaseOpen = true;
|
|
793
|
+
notify();
|
|
794
|
+
}
|
|
795
|
+
if (data.type === "cohub.work.authorize") {
|
|
796
|
+
const allowedViewerScopes = clonePermissionScopes(work.allowedViewerScopes);
|
|
797
|
+
const scopes = clonePermissionScopes(data.scopes).filter((scope) => allowedViewerScopes.includes(scope));
|
|
798
|
+
if (scopes.length === 0) {
|
|
799
|
+
reply(data.requestId, {
|
|
800
|
+
type: "cohub.work.error",
|
|
801
|
+
message: "No allowed scopes requested."
|
|
802
|
+
});
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (isBackground && await isCurrentViewerWorkOwner()) {
|
|
806
|
+
const token = await authorize(scopes);
|
|
807
|
+
reply(data.requestId, {
|
|
808
|
+
type: "cohub.work.authorize.result",
|
|
809
|
+
token
|
|
810
|
+
});
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const viewerUuid = await getViewerUuid();
|
|
814
|
+
if (viewerUuid && hasGrantedWorkScopes(viewerUuid, work.id, scopes)) try {
|
|
815
|
+
const token = await authorize(scopes);
|
|
816
|
+
reply(data.requestId, {
|
|
817
|
+
type: "cohub.work.authorize.result",
|
|
818
|
+
token
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
} catch {
|
|
822
|
+
clearGrantedWorkScopes(viewerUuid, work.id);
|
|
823
|
+
}
|
|
824
|
+
state.pendingAuth = {
|
|
825
|
+
requestId: data.requestId,
|
|
826
|
+
scopes,
|
|
827
|
+
reason: data.reason
|
|
828
|
+
};
|
|
829
|
+
state.authError = null;
|
|
830
|
+
state.authOpen = true;
|
|
831
|
+
notify();
|
|
832
|
+
}
|
|
833
|
+
} catch (error) {
|
|
834
|
+
reply(data.requestId, {
|
|
835
|
+
type: "cohub.work.error",
|
|
836
|
+
message: error instanceof Error ? error.message : "Request failed."
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function cancelAuth() {
|
|
841
|
+
if (state.authSaving) return;
|
|
842
|
+
if (!state.pendingAuth) return;
|
|
843
|
+
reply(state.pendingAuth.requestId, {
|
|
844
|
+
type: "cohub.work.authorize.result",
|
|
845
|
+
token: null
|
|
846
|
+
});
|
|
847
|
+
state.authOpen = false;
|
|
848
|
+
state.pendingAuth = null;
|
|
849
|
+
state.authError = null;
|
|
850
|
+
state.authSaving = false;
|
|
851
|
+
notify();
|
|
852
|
+
}
|
|
853
|
+
function cancelPurchase() {
|
|
854
|
+
if (state.purchaseSaving) return;
|
|
855
|
+
if (!state.pendingPurchase) return;
|
|
856
|
+
reply(state.pendingPurchase.requestId, {
|
|
857
|
+
type: "cohub.work.purchase.result",
|
|
858
|
+
checkout: null
|
|
859
|
+
});
|
|
860
|
+
state.purchaseOpen = false;
|
|
861
|
+
state.purchaseError = null;
|
|
862
|
+
state.pendingPurchase = null;
|
|
863
|
+
state.purchaseSaving = false;
|
|
864
|
+
notify();
|
|
865
|
+
}
|
|
866
|
+
async function confirmPurchase() {
|
|
867
|
+
if (!state.pendingPurchase || state.purchaseSaving) return;
|
|
868
|
+
state.purchaseSaving = true;
|
|
869
|
+
state.purchaseError = null;
|
|
870
|
+
notify();
|
|
871
|
+
try {
|
|
872
|
+
const checkout = await createPurchase(state.pendingPurchase.productKey);
|
|
873
|
+
reply(state.pendingPurchase.requestId, {
|
|
874
|
+
type: "cohub.work.purchase.result",
|
|
875
|
+
checkout
|
|
876
|
+
});
|
|
877
|
+
if (checkout && typeof checkout === "object") {
|
|
878
|
+
const next = checkout;
|
|
879
|
+
if (typeof next.orderId === "string" && typeof next.productKey === "string") writePendingPurchase({
|
|
880
|
+
orderId: next.orderId,
|
|
881
|
+
productKey: next.productKey
|
|
882
|
+
});
|
|
883
|
+
const url = next.checkoutUrl;
|
|
884
|
+
if (next.checkoutUsable === true && typeof url === "string" && url) window.location.href = url;
|
|
885
|
+
}
|
|
886
|
+
state.purchaseOpen = false;
|
|
887
|
+
state.pendingPurchase = null;
|
|
888
|
+
} catch (error) {
|
|
889
|
+
state.purchaseError = error instanceof Error ? error.message : "Purchase failed.";
|
|
890
|
+
} finally {
|
|
891
|
+
state.purchaseSaving = false;
|
|
892
|
+
notify();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async function confirmAuth() {
|
|
896
|
+
if (!state.pendingAuth || state.authSaving) return;
|
|
897
|
+
state.authError = null;
|
|
898
|
+
state.authSaving = true;
|
|
899
|
+
notify();
|
|
900
|
+
try {
|
|
901
|
+
const token = await authorize(state.pendingAuth.scopes);
|
|
902
|
+
setGrantedWorkScopes(await getViewerUuid(), work.id, state.pendingAuth.scopes);
|
|
903
|
+
reply(state.pendingAuth.requestId, {
|
|
904
|
+
type: "cohub.work.authorize.result",
|
|
905
|
+
token
|
|
906
|
+
});
|
|
907
|
+
state.authOpen = false;
|
|
908
|
+
state.pendingAuth = null;
|
|
909
|
+
} catch (error) {
|
|
910
|
+
state.authError = error instanceof Error ? error.message : "Authorization failed.";
|
|
911
|
+
} finally {
|
|
912
|
+
state.authSaving = false;
|
|
913
|
+
notify();
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
getState: () => ({ ...state }),
|
|
918
|
+
handleMessage,
|
|
919
|
+
confirmAuth,
|
|
920
|
+
cancelAuth,
|
|
921
|
+
confirmPurchase,
|
|
922
|
+
cancelPurchase
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
//#endregion
|
|
343
926
|
//#region src/http-error.ts
|
|
344
927
|
function isHttpErrorCode(error, code) {
|
|
345
928
|
return error instanceof HttpError && error.code === code;
|
|
@@ -576,4 +1159,4 @@ function filterGenerationDeclarationsByPolicy(declarations, policy) {
|
|
|
576
1159
|
});
|
|
577
1160
|
}
|
|
578
1161
|
//#endregion
|
|
579
|
-
export { BillingApi, COHUB_ENVIRONMENTS, CohubClient, CohubHttpClient, GenerationPolicyError, HttpError, ReferencesApi, SessionGenerationStreamClient, SessionPatchReducer, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRuntimeApi, WorksApi, assertGenerationRequestAllowedByPolicy, createCohubClient, createHttpClient, createSessionGenerationStreamClient, createSessionPatchReducer, createVoiceInputClient, createWebsocketClient, createWorkRuntime, decodeGenerationPolicy, encodeGenerationPolicy, filterGenerationDeclarationsByPolicy, findGenerationModelPolicy, getAllowedGenerationModelIds, isHttpErrorCode, normalizeBaseUrl, normalizeGenerationPolicy, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, parseAssistantMessageCommit, parseGenerationPolicyFromEnv, resolveApiBaseUrl, resolveCohubEnvironment, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl };
|
|
1162
|
+
export { BillingApi, COHUB_ENVIRONMENTS, CohubClient, CohubHttpClient, GenerationPolicyError, HttpError, ParentBridgeTransport, PopupBrokerTransport, ReferencesApi, SessionGenerationStreamClient, SessionPatchReducer, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRuntimeApi, WorksApi, assertGenerationRequestAllowedByPolicy, clearGrantedWorkScopes, createCohubClient, createHttpClient, createSessionGenerationStreamClient, createSessionPatchReducer, createVoiceInputClient, createWebsocketClient, createWorkBridgeCore, createWorkRuntime, decodeGenerationPolicy, encodeGenerationPolicy, filterGenerationDeclarationsByPolicy, findGenerationModelPolicy, getAllowedGenerationModelIds, hasGrantedWorkScopes, isHttpErrorCode, normalizeBaseUrl, normalizeGenerationPolicy, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, parseAssistantMessageCommit, parseGenerationPolicyFromEnv, resolveApiBaseUrl, resolveCohubEnvironment, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl, resolveWorkTransport, setGrantedWorkScopes };
|