@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/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
- let trustedParentOrigin = null;
113
- const request = (message, timeoutMs = 1200, retryIntervalMs) => {
114
- if (!hasParent()) return Promise.resolve(null);
115
- const requestId = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
116
- return new Promise((resolve, reject) => {
117
- let retryTimer = null;
118
- const parentOrigin = trustedParentOrigin ?? getParentOrigin();
119
- const postRequest = () => {
120
- try {
121
- window.parent.postMessage({
122
- ...message,
123
- requestId
124
- }, parentOrigin ?? "*");
125
- } catch {
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
- const cleanup = () => {
130
- clearTimeout(timer);
131
- if (retryTimer) clearInterval(retryTimer);
132
- window.removeEventListener("message", onMessage);
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
- resolve(data);
150
- };
151
- window.addEventListener("message", onMessage);
152
- postRequest();
153
- if (retryIntervalMs) retryTimer = setInterval(postRequest, retryIntervalMs);
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" }, 8e3, 250))?.context ?? null;
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
- const response = await request({
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" }, 8e3, 250) ?? null;
338
+ return await this.transport.request({ type: "cohub.work.checkout-state" }, {
339
+ timeoutMs: 8e3,
340
+ retryIntervalMs: 250
341
+ }) ?? null;
187
342
  }
188
343
  };
189
- const createWorkRuntime = () => new WorkRuntimeApi();
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
- this.workRuntime = createWorkRuntime();
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 };