@stacksjs/desktop 0.2.175 → 0.2.177

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.
@@ -25,20 +25,20 @@
25
25
  "/**\n * Global Shortcuts (system-level hotkeys)\n *\n * **Different from `hotkeys.ts`.** That module registers in-window\n * shortcuts via document-level keyboard listeners — they only fire\n * while the Craft window has focus. This module talks to the native\n * `craft.shortcuts` bridge, which uses `RegisterEventHotKey` (macOS) /\n * `RegisterHotKey` (Windows) to make shortcuts fire **even when the\n * app isn't focused.** Choose whichever matches your use case.\n *\n * Browser builds: this module is a graceful no-op (you can call it,\n * but the shortcuts won't fire). Use `hotkeys.ts` for in-window-only\n * shortcuts that should work everywhere.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface GlobalShortcutOptions {\n /**\n * If true, the shortcut continues to fire while the user holds the\n * keys down. Defaults to false (single fire on press).\n */\n repeats?: boolean\n}\n\nexport interface ShortcutFireEvent {\n /** The id passed at registration time. */\n id: string\n /** The accelerator string that triggered this fire. */\n accelerator: string\n /** Epoch ms when the OS dispatched the shortcut. */\n timestamp: number\n}\n\nexport interface GlobalShortcuts {\n /**\n * Register a system-wide hotkey under a stable id.\n * @param id - your identifier; reuse to listen via `on()`.\n * @param accelerator - e.g. `'Cmd+Shift+P'`, `'Ctrl+Alt+Space'`.\n */\n register: (id: string, accelerator: string, opts?: GlobalShortcutOptions) => Promise<void>\n /** Remove a previously-registered hotkey. */\n unregister: (id: string) => Promise<void>\n /** Remove every shortcut this app has registered. */\n unregisterAll: () => Promise<void>\n /** Re-enable a temporarily-disabled shortcut. */\n enable: (id: string) => Promise<void>\n /** Disable a shortcut without removing its registration. */\n disable: (id: string) => Promise<void>\n /** Returns true if `id` is currently registered. */\n isRegistered: (id: string) => Promise<boolean>\n /** List every registered shortcut for this app. */\n list: () => Promise<Array<{ id: string, accelerator: string, enabled: boolean }>>\n /** Subscribe to fire events. Returns an unsubscribe function. */\n on: (cb: (e: ShortcutFireEvent) => void) => () => void\n}\n\nexport const globalShortcuts: GlobalShortcuts = {\n async register(id, accelerator, opts) {\n if (!hasBridge('shortcuts')) return\n await window.craft!.shortcuts.register(id, accelerator, opts)\n },\n async unregister(id) {\n if (!hasBridge('shortcuts')) return\n await window.craft!.shortcuts.unregister(id)\n },\n async unregisterAll() {\n if (!hasBridge('shortcuts')) return\n await window.craft!.shortcuts.unregisterAll()\n },\n async enable(id) {\n if (!hasBridge('shortcuts')) return\n await window.craft!.shortcuts.enable(id)\n },\n async disable(id) {\n if (!hasBridge('shortcuts')) return\n await window.craft!.shortcuts.disable(id)\n },\n async isRegistered(id) {\n if (!hasBridge('shortcuts')) return false\n return await window.craft!.shortcuts.isRegistered(id)\n },\n async list() {\n if (!hasBridge('shortcuts')) return []\n return await window.craft!.shortcuts.list()\n },\n on(cb) {\n return onCraftEvent<ShortcutFireEvent>('craft:shortcut', cb)\n },\n}\n",
26
26
  "/**\n * Handoff (Apple Continuity)\n *\n * `NSUserActivity` lets one of the user's Apple devices pick up a\n * task started on another. A reading app can publish \"currently\n * reading X at chapter Y\" and have the user's iPad pick up exactly\n * where they left off, etc.\n *\n * The bridge exposes:\n *\n * - `startActivity(type, opts)` — broadcast a new activity. The\n * `type` is the activity name\n * declared in `Info.plist`'s\n * `NSUserActivityTypes`.\n * - `updateActivity(opts)` — mutate the in-flight activity.\n * - `stopActivity()` — invalidate.\n * - `getCurrentActivity()` — read the current snapshot.\n * - `onIncoming(cb)` — subscribe to incoming handoffs\n * from another device.\n *\n * **Required Info.plist setup:** add a `NSUserActivityTypes` array\n * listing the activity-type strings your app will use. Without that\n * declaration, macOS rejects the activity at registration time.\n *\n * Browser fallback: this module is a graceful no-op (subscriptions\n * never fire). Handoff has no web equivalent.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface HandoffActivityOptions {\n /** Human-readable title shown in the Handoff UI. */\n title?: string\n /** URL to fall back to on devices without the app installed. */\n webpageURL?: string\n /**\n * Arbitrary state to send to the receiving device. Must be plist-\n * compatible (strings, numbers, bools, arrays, dicts of the same).\n * Round-trips through NSJSONSerialization.\n */\n userInfo?: Record<string, unknown>\n}\n\nexport interface HandoffSnapshot {\n type: string\n title: string\n webpageURL: string\n}\n\nexport interface HandoffIncomingEvent {\n type: string\n title?: string\n webpageURL?: string\n userInfo?: Record<string, unknown>\n}\n\nexport interface HandoffAPI {\n /** Start broadcasting a new activity. Resolves to true on success. */\n startActivity: (type: string, options?: HandoffActivityOptions) => Promise<boolean>\n /** Update the in-flight activity (title / webpageURL / userInfo). */\n updateActivity: (options: HandoffActivityOptions) => Promise<boolean>\n /** Invalidate the current activity. Idempotent. */\n stopActivity: () => Promise<void>\n /** Read the current activity, or null if none. */\n getCurrentActivity: () => Promise<HandoffSnapshot | null>\n /** Subscribe to incoming handoffs from another device. */\n onIncoming: (cb: (event: HandoffIncomingEvent) => void) => () => void\n}\n\nexport const handoff: HandoffAPI = {\n async startActivity(type, options) {\n if (!type) throw new Error('handoff.startActivity: type is required')\n if (!hasBridge('handoff')) return false\n // Tolerate both shapes: the production JS facade in craft-bridge.js\n // extracts `{ok:boolean}` to a bare boolean, but the TS-only mock\n // path returns the raw `{ok}` envelope. Either way we surface a\n // single boolean to the caller so call sites stay clean.\n const r = await window.craft!.handoff.startActivity(type, options)\n return typeof r === 'boolean' ? r : !!(r && r.ok)\n },\n async updateActivity(options) {\n if (!hasBridge('handoff')) return false\n const r = await window.craft!.handoff.updateActivity(options)\n return typeof r === 'boolean' ? r : !!(r && r.ok)\n },\n async stopActivity() {\n if (!hasBridge('handoff')) return\n await window.craft!.handoff.stopActivity()\n },\n async getCurrentActivity() {\n if (!hasBridge('handoff')) return null\n const r = await window.craft!.handoff.getCurrentActivity()\n return r && typeof r.type === 'string' ? r : null\n },\n onIncoming(cb) {\n return onCraftEvent<HandoffIncomingEvent>('craft:handoff:incoming', cb)\n },\n}\n",
27
27
  "/**\n * Hotkey API (in-window, document-level)\n *\n * Register keyboard shortcuts via document-level listeners. These fire\n * **only while a Craft window has focus** — a thin wrapper around\n * `keydown`. Use this for app-internal shortcuts like\n * \"Cmd+K opens the command palette.\"\n *\n * **For system-wide shortcuts** that fire even when the app isn't\n * focused (e.g. global \"Cmd+Shift+V to bring up your clipboard\n * manager\"), use `globalShortcuts` from `./global-shortcuts` — that\n * talks to the native `RegisterEventHotKey` / `RegisterHotKey` APIs.\n *\n * @example\n * ```typescript\n * import { registerHotkey, unregisterAllHotkeys } from '@stacksjs/desktop'\n *\n * const reg = registerHotkey('Cmd+Shift+C', () => {\n * console.log('Hotkey triggered!')\n * })\n *\n * reg.unregister()\n * unregisterAllHotkeys()\n * ```\n */\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface HotkeyRegistration {\n /** The shortcut string (e.g., 'Cmd+Shift+C') */\n shortcut: string\n /** Unique registration ID */\n id: string\n /** Unregister this hotkey */\n unregister(): void\n}\n\nexport interface ParsedShortcut {\n /** Key character or name (e.g., 'c', 'Enter', 'Space') */\n key: string\n /** Meta/Command key required */\n meta: boolean\n /** Control key required */\n ctrl: boolean\n /** Shift key required */\n shift: boolean\n /** Alt/Option key required */\n alt: boolean\n}\n\n// ============================================================================\n// Internal State\n// ============================================================================\n\nconst registrations = new Map<string, { shortcut: string, handler: () => void, parsed: ParsedShortcut }>()\nlet nextId = 0\nlet documentListenerAttached = false\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Parse a shortcut string into its component parts.\n *\n * Supports formats like:\n * - 'Cmd+Shift+C'\n * - 'Ctrl+Alt+Delete'\n * - 'Meta+K'\n * - 'CmdOrCtrl+S' (Cmd on macOS, Ctrl on others)\n */\nexport function parseShortcut(shortcut: string): ParsedShortcut {\n const parts = shortcut.split('+').map(p => p.trim())\n const result: ParsedShortcut = {\n key: '',\n meta: false,\n ctrl: false,\n shift: false,\n alt: false,\n }\n\n for (const part of parts) {\n const lower = part.toLowerCase()\n\n switch (lower) {\n case 'cmd':\n case 'command':\n case 'meta':\n case '⌘':\n result.meta = true\n break\n case 'ctrl':\n case 'control':\n case '⌃':\n result.ctrl = true\n break\n case 'shift':\n case '⇧':\n result.shift = true\n break\n case 'alt':\n case 'option':\n case 'opt':\n case '⌥':\n result.alt = true\n break\n case 'cmdorctrl':\n case 'commandorcontrol':\n // On macOS use Meta, otherwise Ctrl\n if (typeof process !== 'undefined' && process.platform === 'darwin') {\n result.meta = true\n }\n else {\n result.ctrl = true\n }\n break\n default:\n result.key = lower\n }\n }\n\n return result\n}\n\n/**\n * Format a parsed shortcut back into a display string.\n */\nexport function formatShortcut(parsed: ParsedShortcut): string {\n const parts: string[] = []\n if (parsed.ctrl)\n parts.push('⌃')\n if (parsed.alt)\n parts.push('⌥')\n if (parsed.shift)\n parts.push('⇧')\n if (parsed.meta)\n parts.push('⌘')\n parts.push(parsed.key.toUpperCase())\n return parts.join('')\n}\n\nfunction matchesEvent(event: KeyboardEvent, parsed: ParsedShortcut): boolean {\n if (parsed.meta !== event.metaKey)\n return false\n if (parsed.ctrl !== event.ctrlKey)\n return false\n if (parsed.shift !== event.shiftKey)\n return false\n if (parsed.alt !== event.altKey)\n return false\n\n const eventKey = event.key.toLowerCase()\n return eventKey === parsed.key || event.code.toLowerCase() === `key${parsed.key}`\n}\n\nfunction handleKeyDown(event: KeyboardEvent): void {\n for (const [, reg] of registrations) {\n if (matchesEvent(event, reg.parsed)) {\n event.preventDefault()\n event.stopPropagation()\n try {\n reg.handler()\n }\n catch {}\n break\n }\n }\n}\n\nfunction ensureDocumentListener(): void {\n if (documentListenerAttached)\n return\n if (typeof document === 'undefined')\n return\n\n document.addEventListener('keydown', handleKeyDown, true)\n documentListenerAttached = true\n}\n\nfunction removeDocumentListener(): void {\n if (!documentListenerAttached)\n return\n if (typeof document === 'undefined')\n return\n\n document.removeEventListener('keydown', handleKeyDown, true)\n documentListenerAttached = false\n}\n\n// ============================================================================\n// Craft Native Integration\n// ============================================================================\n\nasync function registerWithCraft(id: string, shortcut: string): Promise<boolean> {\n if (typeof window === 'undefined')\n return false\n\n const craft = (window as any).craft\n if (!craft?.hotkeys?.register)\n return false\n\n try {\n await craft.hotkeys.register(id, shortcut)\n return true\n }\n catch {\n return false\n }\n}\n\nasync function unregisterWithCraft(id: string): Promise<void> {\n if (typeof window === 'undefined')\n return\n\n const craft = (window as any).craft\n if (!craft?.hotkeys?.unregister)\n return\n\n try {\n await craft.hotkeys.unregister(id)\n }\n catch {}\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Register a global keyboard shortcut.\n *\n * When running in a Craft native window, this registers a system-wide hotkey\n * that works even when the app is not focused. In web mode, it falls back to\n * a document-level keydown listener.\n *\n * @param shortcut - The keyboard shortcut (e.g., 'Cmd+Shift+C', 'CmdOrCtrl+K')\n * @param handler - Function to call when the shortcut is triggered\n * @returns A registration object with an `unregister()` method\n */\nexport function registerHotkey(shortcut: string, handler: () => void): HotkeyRegistration {\n const id = `hotkey_${++nextId}_${Date.now()}`\n const parsed = parseShortcut(shortcut)\n\n registrations.set(id, { shortcut, handler, parsed })\n\n // Try to register with Craft native side\n registerWithCraft(id, shortcut)\n\n // Always set up document listener as fallback\n ensureDocumentListener()\n\n const registration: HotkeyRegistration = {\n shortcut,\n id,\n unregister() {\n unregisterHotkey(registration)\n },\n }\n\n return registration\n}\n\n/**\n * Unregister a specific hotkey.\n */\nexport function unregisterHotkey(registration: HotkeyRegistration): void {\n registrations.delete(registration.id)\n unregisterWithCraft(registration.id)\n\n if (registrations.size === 0) {\n removeDocumentListener()\n }\n}\n\n/**\n * Unregister all registered hotkeys.\n */\nexport function unregisterAllHotkeys(): void {\n for (const [id] of registrations) {\n unregisterWithCraft(id)\n }\n registrations.clear()\n removeDocumentListener()\n}\n\n/**\n * Get all currently registered hotkeys.\n */\nexport function getRegisteredHotkeys(): HotkeyRegistration[] {\n return Array.from(registrations.entries()).map(([id, reg]) => ({\n shortcut: reg.shortcut,\n id,\n unregister() {\n registrations.delete(id)\n unregisterWithCraft(id)\n if (registrations.size === 0) {\n removeDocumentListener()\n }\n },\n }))\n}\n",
28
- "/**\n * In-App Purchases (StoreKit on macOS)\n *\n * **Scope**: minimal. Full StoreKit support involves SKProductsRequest\n * delegates, transaction observers, receipt validation, family sharing,\n * promotional offers — months of work. The native side currently\n * implements `isAvailable`, `restorePurchases`, and `getReceiptData`\n * fully; `getProducts`, `purchase`, and `finishTransaction` are stubs\n * pending the StoreKit observer wiring.\n *\n * Apps that need a working IAP flow today should:\n * 1. Use `getReceiptData()` to grab the bundled App Store receipt\n * and verify it server-side via Apple's verifyReceipt API.\n * 2. Implement product fetch + purchase via a separate SDK or\n * direct StoreKit code in your bundle.\n *\n * This shape is intentionally stable so apps can write against it now\n * and switch over to a richer implementation when it lands.\n */\nimport { hasBridge } from './_bridge'\n\nexport type IAPProductType = \n| 'consumable'\n| 'non-consumable'\n| 'auto-subscription'\n| 'non-auto-subscription'\n\nexport type IAPSubscriptionPeriod = 'day' | 'week' | 'month' | 'year'\n\nexport interface IAPSubscriptionInfo {\n /** Length of one period — `1` + `month` means a one-month subscription. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** Free trial / intro offer attached to this subscription, if any. */\n introductoryOffer?: IAPIntroductoryOffer\n /** True when the subscription is shareable with iCloud Family. */\n familyShareable?: boolean\n /** Subscription group identifier. Used by StoreKit for upgrade/downgrade math. */\n groupIdentifier?: string\n}\n\nexport interface IAPIntroductoryOffer {\n /** \"free-trial\" | \"pay-as-you-go\" | \"pay-up-front\". */\n paymentMode: 'free-trial' | 'pay-as-you-go' | 'pay-up-front'\n /** Localized price including currency symbol; `\"0.00\"` for free trials. */\n localizedPrice: string\n /** How long the intro lasts. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** How many billing periods the intro repeats over (>=1). */\n numberOfPeriods?: number\n}\n\nexport interface IAPProduct {\n id: string\n title: string\n description?: string\n price: string\n /** Currency code (e.g. \"USD\"). */\n currency?: string\n /** Localized price including currency symbol. */\n localizedPrice?: string\n type?: IAPProductType\n /** Present only when `type === 'auto-subscription'`. */\n subscription?: IAPSubscriptionInfo\n}\n\nexport interface IAPPurchaseResult {\n /** True when the purchase was queued successfully. */\n queued: boolean\n productId?: string\n /** Why the purchase couldn't be queued (transient stub limitation, etc). */\n reason?: string\n}\n\nexport interface IAPTransactionEvent {\n productId: string\n transactionId: string\n /** ISO date string. */\n date?: string\n /** Original transaction id — populated for renewals / restores. */\n originalTransactionId?: string\n /** True when this purchase was issued under iCloud Family Sharing. */\n familyShared?: boolean\n /** True when restoring a previously purchased non-consumable. */\n restored?: boolean\n /** Subscription auto-renewal status, if applicable. */\n autoRenewing?: boolean\n /** ISO date when the current subscription period expires. */\n expiresAt?: string\n /** True when StoreKit reports this transaction is in the intro/free-trial period. */\n inIntroPeriod?: boolean\n}\n\nexport interface IAPFailureEvent {\n productId: string\n /** Apple's SKErrorCode value. */\n code?: number\n message?: string\n}\n\nexport interface IAPRefundEvent {\n productId: string\n transactionId: string\n /** ISO date when the refund was issued. */\n refundedAt?: string\n /** \"voluntary\" | \"issue-app\" | \"other\" — Apple's refund preference. */\n reason?: string\n}\n\nexport interface IAPSubscriptionStatusEvent {\n productId: string\n /** \"active\" | \"expired\" | \"in-grace-period\" | \"in-billing-retry\" | \"revoked\". */\n status: 'active' | 'expired' | 'in-grace-period' | 'in-billing-retry' | 'revoked'\n /** ISO date when status takes effect. */\n changedAt?: string\n /** ISO date the current period ends. */\n expiresAt?: string\n}\n\nexport interface IAPAPI {\n /** True if the device is allowed to make payments. */\n isAvailable: () => Promise<boolean>\n /** Fetch products by id. May return an empty array if native fetch isn't wired yet. */\n getProducts: (ids: string[] | string) => Promise<IAPProduct[]>\n /** Queue a purchase. Result fires via `onPurchased` / `onFailed`. */\n purchase: (productId: string) => Promise<IAPPurchaseResult>\n /** Restore previously-bought non-consumables tied to the user's Apple ID. */\n restorePurchases: () => Promise<{ ok: boolean }>\n /**\n * Mark a transaction as finished — required for non-consumables to\n * stop StoreKit re-delivering them.\n */\n finishTransaction: (transactionId: string) => Promise<void>\n /**\n * Read the App Store receipt as a base64 string. Hand this to your\n * server and call Apple's `verifyReceipt` for trusted validation.\n */\n getReceiptData: () => Promise<string | null>\n /** Subscribe to successful purchase events. */\n onPurchased: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to purchase-failure events. */\n onFailed: (cb: (e: IAPFailureEvent) => void) => () => void\n /** Subscribe to \"purchase restored\" events. */\n onRestored: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to async products-fetch results. */\n onProductsLoaded: (cb: (products: IAPProduct[]) => void) => () => void\n /**\n * Fires when a previous purchase has been refunded by the user via the\n * App Store. Apps should immediately revoke the entitlement granted by\n * the original transaction.\n */\n onRefunded: (cb: (e: IAPRefundEvent) => void) => () => void\n /**\n * Fires whenever a subscription's lifecycle status changes — the user\n * lapses out of grace period, billing retry resolves, etc.\n */\n onSubscriptionStatusChanged: (cb: (e: IAPSubscriptionStatusEvent) => void) => () => void\n /**\n * Returns the currently-active subscription product ids, plus their\n * status. Use on app boot to gate features rather than re-running\n * `restorePurchases()` every time.\n */\n getActiveSubscriptions: () => Promise<IAPSubscriptionStatusEvent[]>\n /**\n * True if the user is eligible for the introductory offer attached to\n * `productId`. Apple gates intro eligibility per subscription group:\n * if the user has ever subscribed to *any* product in the same group,\n * they're ineligible for further intro offers.\n */\n isEligibleForIntroOffer: (productId: string) => Promise<boolean>\n}\n\nimport { onCraftEvent } from './_bridge'\n\nexport const iap: IAPAPI = {\n async isAvailable() {\n if (!hasBridge('iap')) return false\n return await window.craft!.iap.isAvailable()\n },\n async getProducts(ids) {\n if (!hasBridge('iap')) return []\n // Normalize to array at the TS boundary so the bridge always sees\n // the same shape, regardless of whether the caller passed a\n // single id or a list.\n const arr = Array.isArray(ids) ? ids : [String(ids)]\n return await window.craft!.iap.getProducts(arr)\n },\n async purchase(productId) {\n if (!hasBridge('iap')) return { queued: false, productId, reason: 'IAP bridge not available' }\n const r = await window.craft!.iap.purchase(productId)\n return { queued: !!(r && r.queued), productId: r?.productId, reason: r?.reason }\n },\n async restorePurchases() {\n if (!hasBridge('iap')) return { ok: false }\n const r = await window.craft!.iap.restorePurchases()\n return { ok: !!(r && r.ok) }\n },\n async finishTransaction(transactionId) {\n if (!hasBridge('iap')) return\n await window.craft!.iap.finishTransaction(transactionId)\n },\n async getReceiptData() {\n if (!hasBridge('iap')) return null\n const r = await window.craft!.iap.getReceiptData()\n return r ? String(r) : null\n },\n onPurchased(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:purchased', cb) },\n onFailed(cb) { return onCraftEvent<IAPFailureEvent>('craft:iap:failed', cb) },\n onRestored(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:restored', cb) },\n onProductsLoaded(cb) {\n return onCraftEvent<{ products?: IAPProduct[] }>('craft:iap:productsLoaded', (e) => cb(e.products || []))\n },\n onRefunded(cb) { return onCraftEvent<IAPRefundEvent>('craft:iap:refunded', cb) },\n onSubscriptionStatusChanged(cb) {\n return onCraftEvent<IAPSubscriptionStatusEvent>('craft:iap:subscriptionStatusChanged', cb)\n },\n async getActiveSubscriptions() {\n if (!hasBridge('iap')) return []\n // Older bridges may not implement this — defensive default keeps the\n // call shape stable so callers can ship before the native side ships.\n const fn = window.craft!.iap.getActiveSubscriptions\n if (typeof fn !== 'function') return []\n const r = await fn()\n return Array.isArray(r) ? r as IAPSubscriptionStatusEvent[] : []\n },\n async isEligibleForIntroOffer(productId) {\n if (!hasBridge('iap')) return false\n const fn = window.craft!.iap.isEligibleForIntroOffer\n if (typeof fn !== 'function') return false\n return !!(await fn(productId))\n },\n}\n",
28
+ "/**\n * In-App Purchases (StoreKit on macOS)\n *\n * **Scope**: minimal. Full StoreKit support involves SKProductsRequest\n * delegates, transaction observers, receipt validation, family sharing,\n * promotional offers — months of work. The native side currently\n * implements `isAvailable`, `restorePurchases`, and `getReceiptData`\n * fully; `getProducts`, `purchase`, and `finishTransaction` are stubs\n * pending the StoreKit observer wiring.\n *\n * Apps that need a working IAP flow today should:\n * 1. Use `getReceiptData()` to grab the bundled App Store receipt\n * and verify it server-side via Apple's verifyReceipt API.\n * 2. Implement product fetch + purchase via a separate SDK or\n * direct StoreKit code in your bundle.\n *\n * This shape is intentionally stable so apps can write against it now\n * and switch over to a richer implementation when it lands.\n */\nimport { hasBridge } from './_bridge'\n\nexport type IAPProductType =\n| 'consumable'\n| 'non-consumable'\n| 'auto-subscription'\n| 'non-auto-subscription'\n\nexport type IAPSubscriptionPeriod = 'day' | 'week' | 'month' | 'year'\n\nexport interface IAPSubscriptionInfo {\n /** Length of one period — `1` + `month` means a one-month subscription. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** Free trial / intro offer attached to this subscription, if any. */\n introductoryOffer?: IAPIntroductoryOffer\n /** True when the subscription is shareable with iCloud Family. */\n familyShareable?: boolean\n /** Subscription group identifier. Used by StoreKit for upgrade/downgrade math. */\n groupIdentifier?: string\n}\n\nexport interface IAPIntroductoryOffer {\n /** \"free-trial\" | \"pay-as-you-go\" | \"pay-up-front\". */\n paymentMode: 'free-trial' | 'pay-as-you-go' | 'pay-up-front'\n /** Localized price including currency symbol; `\"0.00\"` for free trials. */\n localizedPrice: string\n /** How long the intro lasts. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** How many billing periods the intro repeats over (>=1). */\n numberOfPeriods?: number\n}\n\nexport interface IAPProduct {\n id: string\n title: string\n description?: string\n price: string\n /** Currency code (e.g. \"USD\"). */\n currency?: string\n /** Localized price including currency symbol. */\n localizedPrice?: string\n type?: IAPProductType\n /** Present only when `type === 'auto-subscription'`. */\n subscription?: IAPSubscriptionInfo\n}\n\nexport interface IAPPurchaseResult {\n /** True when the purchase was queued successfully. */\n queued: boolean\n productId?: string\n /** Why the purchase couldn't be queued (transient stub limitation, etc). */\n reason?: string\n}\n\nexport interface IAPTransactionEvent {\n productId: string\n transactionId: string\n /** ISO date string. */\n date?: string\n /** Original transaction id — populated for renewals / restores. */\n originalTransactionId?: string\n /** True when this purchase was issued under iCloud Family Sharing. */\n familyShared?: boolean\n /** True when restoring a previously purchased non-consumable. */\n restored?: boolean\n /** Subscription auto-renewal status, if applicable. */\n autoRenewing?: boolean\n /** ISO date when the current subscription period expires. */\n expiresAt?: string\n /** True when StoreKit reports this transaction is in the intro/free-trial period. */\n inIntroPeriod?: boolean\n}\n\nexport interface IAPFailureEvent {\n productId: string\n /** Apple's SKErrorCode value. */\n code?: number\n message?: string\n}\n\nexport interface IAPRefundEvent {\n productId: string\n transactionId: string\n /** ISO date when the refund was issued. */\n refundedAt?: string\n /** \"voluntary\" | \"issue-app\" | \"other\" — Apple's refund preference. */\n reason?: string\n}\n\nexport interface IAPSubscriptionStatusEvent {\n productId: string\n /** \"active\" | \"expired\" | \"in-grace-period\" | \"in-billing-retry\" | \"revoked\". */\n status: 'active' | 'expired' | 'in-grace-period' | 'in-billing-retry' | 'revoked'\n /** ISO date when status takes effect. */\n changedAt?: string\n /** ISO date the current period ends. */\n expiresAt?: string\n}\n\nexport interface IAPAPI {\n /** True if the device is allowed to make payments. */\n isAvailable: () => Promise<boolean>\n /** Fetch products by id. May return an empty array if native fetch isn't wired yet. */\n getProducts: (ids: string[] | string) => Promise<IAPProduct[]>\n /** Queue a purchase. Result fires via `onPurchased` / `onFailed`. */\n purchase: (productId: string) => Promise<IAPPurchaseResult>\n /** Restore previously-bought non-consumables tied to the user's Apple ID. */\n restorePurchases: () => Promise<{ ok: boolean }>\n /**\n * Mark a transaction as finished — required for non-consumables to\n * stop StoreKit re-delivering them.\n */\n finishTransaction: (transactionId: string) => Promise<void>\n /**\n * Read the App Store receipt as a base64 string. Hand this to your\n * server and call Apple's `verifyReceipt` for trusted validation.\n */\n getReceiptData: () => Promise<string | null>\n /** Subscribe to successful purchase events. */\n onPurchased: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to purchase-failure events. */\n onFailed: (cb: (e: IAPFailureEvent) => void) => () => void\n /** Subscribe to \"purchase restored\" events. */\n onRestored: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to async products-fetch results. */\n onProductsLoaded: (cb: (products: IAPProduct[]) => void) => () => void\n /**\n * Fires when a previous purchase has been refunded by the user via the\n * App Store. Apps should immediately revoke the entitlement granted by\n * the original transaction.\n */\n onRefunded: (cb: (e: IAPRefundEvent) => void) => () => void\n /**\n * Fires whenever a subscription's lifecycle status changes — the user\n * lapses out of grace period, billing retry resolves, etc.\n */\n onSubscriptionStatusChanged: (cb: (e: IAPSubscriptionStatusEvent) => void) => () => void\n /**\n * Returns the currently-active subscription product ids, plus their\n * status. Use on app boot to gate features rather than re-running\n * `restorePurchases()` every time.\n */\n getActiveSubscriptions: () => Promise<IAPSubscriptionStatusEvent[]>\n /**\n * True if the user is eligible for the introductory offer attached to\n * `productId`. Apple gates intro eligibility per subscription group:\n * if the user has ever subscribed to *any* product in the same group,\n * they're ineligible for further intro offers.\n */\n isEligibleForIntroOffer: (productId: string) => Promise<boolean>\n}\n\nimport { onCraftEvent } from './_bridge'\n\nexport const iap: IAPAPI = {\n async isAvailable() {\n if (!hasBridge('iap')) return false\n return await window.craft!.iap.isAvailable()\n },\n async getProducts(ids) {\n if (!hasBridge('iap')) return []\n // Normalize to array at the TS boundary so the bridge always sees\n // the same shape, regardless of whether the caller passed a\n // single id or a list.\n const arr = Array.isArray(ids) ? ids : [String(ids)]\n return await window.craft!.iap.getProducts(arr)\n },\n async purchase(productId) {\n if (!hasBridge('iap')) return { queued: false, productId, reason: 'IAP bridge not available' }\n const r = await window.craft!.iap.purchase(productId)\n return { queued: !!(r && r.queued), productId: r?.productId, reason: r?.reason }\n },\n async restorePurchases() {\n if (!hasBridge('iap')) return { ok: false }\n const r = await window.craft!.iap.restorePurchases()\n return { ok: !!(r && r.ok) }\n },\n async finishTransaction(transactionId) {\n if (!hasBridge('iap')) return\n await window.craft!.iap.finishTransaction(transactionId)\n },\n async getReceiptData() {\n if (!hasBridge('iap')) return null\n const r = await window.craft!.iap.getReceiptData()\n return r ? String(r) : null\n },\n onPurchased(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:purchased', cb) },\n onFailed(cb) { return onCraftEvent<IAPFailureEvent>('craft:iap:failed', cb) },\n onRestored(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:restored', cb) },\n onProductsLoaded(cb) {\n return onCraftEvent<{ products?: IAPProduct[] }>('craft:iap:productsLoaded', (e) => cb(e.products || []))\n },\n onRefunded(cb) { return onCraftEvent<IAPRefundEvent>('craft:iap:refunded', cb) },\n onSubscriptionStatusChanged(cb) {\n return onCraftEvent<IAPSubscriptionStatusEvent>('craft:iap:subscriptionStatusChanged', cb)\n },\n async getActiveSubscriptions() {\n if (!hasBridge('iap')) return []\n // Older bridges may not implement this — defensive default keeps the\n // call shape stable so callers can ship before the native side ships.\n const fn = window.craft!.iap.getActiveSubscriptions\n if (typeof fn !== 'function') return []\n const r = await fn()\n return Array.isArray(r) ? r as IAPSubscriptionStatusEvent[] : []\n },\n async isEligibleForIntroOffer(productId) {\n if (!hasBridge('iap')) return false\n const fn = window.craft!.iap.isEligibleForIntroOffer\n if (typeof fn !== 'function') return false\n return !!(await fn(productId))\n },\n}\n",
29
29
  "/**\n * Keychain — Secure Secret Storage\n *\n * Wraps platform-specific credential stores: macOS Keychain Services,\n * iOS Keychain, Windows Credential Manager, Linux Secret Service\n * (D-Bus / GNOME Keyring).\n *\n * Use this for OAuth refresh tokens, API keys, login passwords —\n * anything you'd be uncomfortable storing in `localStorage`. Items\n * are scoped under a `service` namespace, typically your app's\n * bundle identifier.\n *\n * No web fallback. The whole point is OS-protected storage; falling\n * back to localStorage would silently downgrade security guarantees.\n * Calls outside a Craft window throw.\n */\nimport { requireBridge } from './_bridge'\n\nexport interface KeychainAPI {\n /**\n * Store a secret. Overwrites any existing entry under (service, account).\n * The password may be any UTF-8 string.\n */\n set: (service: string, account: string, password: string) => Promise<void>\n /**\n * Read a secret. Returns `null` (not undefined) if no entry exists,\n * so callers can distinguish \"not found\" from \"found, value is empty\".\n */\n get: (service: string, account: string) => Promise<string | null>\n /** Delete a secret. No-op if it doesn't exist. */\n delete: (service: string, account: string) => Promise<void>\n /** Check whether a secret exists, without reading it (no decrypt cost). */\n has: (service: string, account: string) => Promise<boolean>\n}\n\nexport const keychain: KeychainAPI = {\n async set(service, account, password) {\n if (!service) throw new Error('keychain.set: service is required')\n if (!account) throw new Error('keychain.set: account is required')\n await requireBridge('keychain').set(service, account, password)\n },\n async get(service, account) {\n if (!service) throw new Error('keychain.get: service is required')\n if (!account) throw new Error('keychain.get: account is required')\n const v = await requireBridge('keychain').get(service, account)\n return typeof v === 'string' ? v : null\n },\n async delete(service, account) {\n if (!service) throw new Error('keychain.delete: service is required')\n if (!account) throw new Error('keychain.delete: account is required')\n await requireBridge('keychain').delete(service, account)\n },\n async has(service, account) {\n if (!service) throw new Error('keychain.has: service is required')\n if (!account) throw new Error('keychain.has: account is required')\n return await requireBridge('keychain').has(service, account)\n },\n}\n",
30
30
  "/**\n * Live Activities — a thin wrapper over Handoff.\n *\n * **Apple's \"Live Activities\" are an iOS-only ActivityKit feature**\n * that lights up Lock Screens and Dynamic Island. They require an\n * iOS 16+ Widget Extension target and can't be invoked from a\n * Craft window directly.\n *\n * What this module ships is the *macOS-compatible approximation*:\n * we publish an NSUserActivity that nearby Apple devices (including\n * the user's iPhone, if it's running a companion app) can pick up\n * via Handoff. It's not a Live Activity in the iOS sense, but it\n * exposes the same conceptual API so app code can be future-ready\n * and dual-targeted.\n *\n * For real iOS Live Activities, ship a Widget Extension target and\n * call `Activity.request(...)` from Swift. This module is the\n * cross-platform glue — same surface, best-effort behaviour.\n */\nimport { handoff } from './handoff'\n\nexport interface LiveActivityState {\n /** Title shown in the Lock Screen / nearby device UI. */\n title?: string\n /** URL to fall back to on devices without the app. */\n webpageURL?: string\n /** Free-form payload passed through to the receiving device. */\n state?: Record<string, unknown>\n}\n\nexport interface LiveActivitiesAPI {\n /**\n * Start a live-activity-shaped session. On macOS this maps to an\n * `NSUserActivity` published via Handoff.\n * @param type — the activity type identifier (must be declared in\n * `Info.plist > NSUserActivityTypes`).\n */\n start: (type: string, state?: LiveActivityState) => Promise<boolean>\n /** Push a new state. Devices receiving the activity see the latest. */\n update: (state: LiveActivityState) => Promise<boolean>\n /** End the activity. Idempotent. */\n stop: () => Promise<void>\n}\n\nexport const liveActivities: LiveActivitiesAPI = {\n async start(type, state) {\n return handoff.startActivity(type, {\n title: state?.title,\n webpageURL: state?.webpageURL,\n userInfo: state?.state,\n })\n },\n async update(state) {\n return handoff.updateActivity({\n title: state.title,\n webpageURL: state.webpageURL,\n userInfo: state.state,\n })\n },\n async stop() {\n await handoff.stopActivity()\n },\n}\n",
31
31
  "/**\n * Local HTTP listener — for OAuth callback flows.\n *\n * The native side binds a TCP socket on `127.0.0.1` and accepts one\n * request at a time. Each incoming request fires `craft:localServer:request`\n * with the parsed URL; apps respond via `respond({ status, body })`\n * within 5 seconds (the listener auto-replies with a default 200 page\n * after that to avoid hung sockets).\n *\n * **Typical OAuth flow:**\n * ```ts\n * const { port } = await localServer.start()\n * const off = localServer.onRequest(({ url }) => {\n * const code = new URL(`http://x${url}`).searchParams.get('code')\n * localServer.respond({ status: 200, body: '<h1>Done</h1>' })\n * resolveCode(code)\n * })\n * await shell.openExternal(`https://provider.com/oauth?redirect=http://127.0.0.1:${port}/cb`)\n * ```\n *\n * No web fallback — there's no socket-binding equivalent.\n */\nimport { hasBridge, onCraftEvent, requireBridge } from './_bridge'\n\nexport interface LocalServerStartResult {\n port: number\n started: boolean\n alreadyRunning?: boolean\n reason?: string\n}\n\nexport interface LocalServerRequestEvent {\n /** HTTP method (GET / POST / etc). */\n method: string\n /** Request path including query string (e.g. `/cb?code=abc`). */\n url: string\n}\n\nexport interface LocalServerRespondOptions {\n /** HTTP status code. Default 200. */\n status?: number\n /** Response body. Default \"OK\". */\n body?: string\n /** Content-Type header. Default `text/html; charset=utf-8`. */\n contentType?: string\n}\n\nexport interface LocalServerAPI {\n /**\n * Start the listener. Pass `port: 0` (default) to let the OS pick a\n * free port — read it from the resolved `port` field. Idempotent;\n * a second call while already running returns the existing port.\n */\n start: (port?: number, host?: string) => Promise<LocalServerStartResult>\n stop: () => Promise<void>\n /** Reply to the in-flight request. Must be called within 5s. */\n respond: (options?: LocalServerRespondOptions) => Promise<void>\n onRequest: (cb: (event: LocalServerRequestEvent) => void) => () => void\n\n /**\n * One-shot helper for OAuth flows: start the server, await the first\n * request, capture the URL, respond with a confirmation page, return\n * the URL. Throws if the bridge is unavailable.\n */\n awaitOAuthCallback: (options?: { port?: number, host?: string, timeoutMs?: number, successHTML?: string }) => Promise<{ url: string, port: number }>\n}\n\nexport const localServer: LocalServerAPI = {\n async start(port = 0, host = '127.0.0.1') {\n if (!hasBridge('localServer')) return { port: 0, started: false, reason: 'bridge unavailable' }\n return await window.craft!.localServer.start(port, host)\n },\n async stop() {\n if (!hasBridge('localServer')) return\n await window.craft!.localServer.stop()\n },\n async respond(options) {\n if (!hasBridge('localServer')) return\n await window.craft!.localServer.respond(options || { status: 200, body: 'OK' })\n },\n onRequest(cb) {\n return onCraftEvent<LocalServerRequestEvent>('craft:localServer:request', cb)\n },\n\n async awaitOAuthCallback(options = {}) {\n requireBridge('localServer')\n\n const { port: requestedPort = 0, host = '127.0.0.1', timeoutMs = 5 * 60 * 1000, successHTML } = options\n const start = await this.start(requestedPort, host)\n if (!start.started) throw new Error(`localServer: start failed${start.reason ? ` — ${start.reason}` : ''}`)\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n off()\n this.stop().catch(() => {})\n reject(new Error('localServer: OAuth callback timed out'))\n }, timeoutMs)\n\n const off = this.onRequest(({ url }) => {\n clearTimeout(timer)\n off()\n // Send a friendly success page so the user knows it worked.\n // The page closes itself after a moment — feels more polished\n // than leaving a blank tab open.\n const body = successHTML ?? `<!doctype html><meta charset=\"utf-8\"><title>Done</title>\n<style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#f5f5f7}</style>\n<div><h1>You can close this tab.</h1><p>Returning to the app…</p></div>\n<script>setTimeout(()=>window.close(),1500)</script>`\n this.respond({ status: 200, body, contentType: 'text/html; charset=utf-8' })\n .catch(() => {})\n .finally(() => this.stop().catch(() => {}))\n resolve({ url, port: start.port })\n })\n })\n },\n}\n",
32
- "/**\n * Geolocation (CoreLocation on macOS).\n *\n * Apps that need higher-than-browser-grade accuracy use this module —\n * the macOS native path delivers GPS/WiFi-positioning samples directly\n * from `CLLocationManager`. Browser fallback uses the standard\n * `navigator.geolocation` API, which is good enough for \"what city\n * am I in\" but not for navigation-grade tracking.\n *\n * **Required Info.plist keys** for the permission prompt:\n * - `NSLocationWhenInUseUsageDescription` — when in use only\n * - `NSLocationAlwaysAndWhenInUseUsageDescription` — background access\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type LocationAuthStatus = \n| 'undetermined'\n| 'restricted-or-denied'\n| 'authorizedAlways'\n| 'authorizedWhenInUse'\n| 'not-supported'\n| 'unknown'\n\nexport interface LocationCoordinate {\n latitude: number\n longitude: number\n altitude?: number\n /** Horizontal accuracy in meters. Negative = invalid. */\n horizontalAccuracy?: number\n verticalAccuracy?: number\n /** Speed in m/s. Negative = invalid. */\n speed?: number\n}\n\nexport interface LocationWatchOptions {\n /** `'continuous'` (high accuracy, more battery) or `'significant'`. */\n mode?: 'continuous' | 'significant'\n /** Distance in meters between updates. */\n distanceFilter?: number\n}\n\nexport interface LocationAPI {\n /** Trigger the system permission prompt. */\n requestPermission: (mode?: 'whenInUse' | 'always') => Promise<LocationAuthStatus>\n /** Read current authorization status without prompting. */\n getAuthorization: () => Promise<LocationAuthStatus>\n /**\n * Request a single location sample. The result arrives via `onUpdate`,\n * not as the resolution value of this call (CoreLocation is async).\n */\n getCurrentLocation: () => Promise<{ requested: boolean }>\n /** Start streaming updates via `onUpdate`. */\n startWatching: (options?: LocationWatchOptions) => Promise<boolean>\n /** Stop the update stream. */\n stopWatching: () => Promise<void>\n /** Subscribe to location samples. */\n onUpdate: (cb: (loc: LocationCoordinate) => void) => () => void\n /** Subscribe to errors (e.g. denied, location unavailable). */\n onError: (cb: (err: { message: string }) => void) => () => void\n /** Subscribe to authorization-status changes. */\n onAuthChanged: (cb: (info: { status: LocationAuthStatus }) => void) => () => void\n}\n\nexport const location: LocationAPI = {\n async requestPermission(mode = 'whenInUse') {\n if (hasBridge('location')) return await window.craft!.location.requestPermission(mode)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n // Browser geolocation prompts implicitly on the first\n // getCurrentPosition() call — there's no separate request API.\n return 'undetermined'\n }\n return 'not-supported'\n },\n async getAuthorization() {\n if (hasBridge('location')) return await window.craft!.location.getAuthorization()\n return 'unknown'\n },\n async getCurrentLocation() {\n if (hasBridge('location')) return await window.craft!.location.getCurrentLocation()\n // Web fallback: kick off navigator.geolocation.getCurrentPosition,\n // and synthesize a `craft:location:update` event so apps that\n // subscribe via `onUpdate` see the result through the same channel.\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n ;(navigator as any).geolocation.getCurrentPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n (err: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:error', {\n detail: { message: err.message || String(err) },\n }))\n },\n )\n return { requested: true }\n }\n return { requested: false }\n },\n async startWatching(options) {\n if (hasBridge('location')) return await window.craft!.location.startWatching(options)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const watchId = (navigator as any).geolocation.watchPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n )\n ;(window as any).__craftWebLocationWatchId = watchId\n return true\n }\n return false\n },\n async stopWatching() {\n if (hasBridge('location')) {\n await window.craft!.location.stopWatching()\n return\n }\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const id = (window as any).__craftWebLocationWatchId\n if (id != null) {\n ;(navigator as any).geolocation.clearWatch(id)\n ;(window as any).__craftWebLocationWatchId = null\n }\n }\n },\n onUpdate(cb) { return onCraftEvent<LocationCoordinate>('craft:location:update', cb) },\n onError(cb) { return onCraftEvent<{ message: string }>('craft:location:error', cb) },\n onAuthChanged(cb) { return onCraftEvent<{ status: LocationAuthStatus }>('craft:location:authChanged', cb) },\n}\n",
32
+ "/**\n * Geolocation (CoreLocation on macOS).\n *\n * Apps that need higher-than-browser-grade accuracy use this module —\n * the macOS native path delivers GPS/WiFi-positioning samples directly\n * from `CLLocationManager`. Browser fallback uses the standard\n * `navigator.geolocation` API, which is good enough for \"what city\n * am I in\" but not for navigation-grade tracking.\n *\n * **Required Info.plist keys** for the permission prompt:\n * - `NSLocationWhenInUseUsageDescription` — when in use only\n * - `NSLocationAlwaysAndWhenInUseUsageDescription` — background access\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type LocationAuthStatus =\n| 'undetermined'\n| 'restricted-or-denied'\n| 'authorizedAlways'\n| 'authorizedWhenInUse'\n| 'not-supported'\n| 'unknown'\n\nexport interface LocationCoordinate {\n latitude: number\n longitude: number\n altitude?: number\n /** Horizontal accuracy in meters. Negative = invalid. */\n horizontalAccuracy?: number\n verticalAccuracy?: number\n /** Speed in m/s. Negative = invalid. */\n speed?: number\n}\n\nexport interface LocationWatchOptions {\n /** `'continuous'` (high accuracy, more battery) or `'significant'`. */\n mode?: 'continuous' | 'significant'\n /** Distance in meters between updates. */\n distanceFilter?: number\n}\n\nexport interface LocationAPI {\n /** Trigger the system permission prompt. */\n requestPermission: (mode?: 'whenInUse' | 'always') => Promise<LocationAuthStatus>\n /** Read current authorization status without prompting. */\n getAuthorization: () => Promise<LocationAuthStatus>\n /**\n * Request a single location sample. The result arrives via `onUpdate`,\n * not as the resolution value of this call (CoreLocation is async).\n */\n getCurrentLocation: () => Promise<{ requested: boolean }>\n /** Start streaming updates via `onUpdate`. */\n startWatching: (options?: LocationWatchOptions) => Promise<boolean>\n /** Stop the update stream. */\n stopWatching: () => Promise<void>\n /** Subscribe to location samples. */\n onUpdate: (cb: (loc: LocationCoordinate) => void) => () => void\n /** Subscribe to errors (e.g. denied, location unavailable). */\n onError: (cb: (err: { message: string }) => void) => () => void\n /** Subscribe to authorization-status changes. */\n onAuthChanged: (cb: (info: { status: LocationAuthStatus }) => void) => () => void\n}\n\nexport const location: LocationAPI = {\n async requestPermission(mode = 'whenInUse') {\n if (hasBridge('location')) return await window.craft!.location.requestPermission(mode)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n // Browser geolocation prompts implicitly on the first\n // getCurrentPosition() call — there's no separate request API.\n return 'undetermined'\n }\n return 'not-supported'\n },\n async getAuthorization() {\n if (hasBridge('location')) return await window.craft!.location.getAuthorization()\n return 'unknown'\n },\n async getCurrentLocation() {\n if (hasBridge('location')) return await window.craft!.location.getCurrentLocation()\n // Web fallback: kick off navigator.geolocation.getCurrentPosition,\n // and synthesize a `craft:location:update` event so apps that\n // subscribe via `onUpdate` see the result through the same channel.\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n ;(navigator as any).geolocation.getCurrentPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n (err: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:error', {\n detail: { message: err.message || String(err) },\n }))\n },\n )\n return { requested: true }\n }\n return { requested: false }\n },\n async startWatching(options) {\n if (hasBridge('location')) return await window.craft!.location.startWatching(options)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const watchId = (navigator as any).geolocation.watchPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n )\n ;(window as any).__craftWebLocationWatchId = watchId\n return true\n }\n return false\n },\n async stopWatching() {\n if (hasBridge('location')) {\n await window.craft!.location.stopWatching()\n return\n }\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const id = (window as any).__craftWebLocationWatchId\n if (id != null) {\n ;(navigator as any).geolocation.clearWatch(id)\n ;(window as any).__craftWebLocationWatchId = null\n }\n }\n },\n onUpdate(cb) { return onCraftEvent<LocationCoordinate>('craft:location:update', cb) },\n onError(cb) { return onCraftEvent<{ message: string }>('craft:location:error', cb) },\n onAuthChanged(cb) { return onCraftEvent<{ status: LocationAuthStatus }>('craft:location:authChanged', cb) },\n}\n",
33
33
  "/**\n * Unified system log (`os_log` on macOS).\n *\n * Apps call `log.{debug,info,warn,error}(message)` and entries land\n * in Console.app, `log show`, and any aggregator the user has wired\n * up (e.g. log-shipper daemons, MDM agents).\n *\n * Outside a Craft window, falls through to the equivalent\n * `console.*` call so library code can call `log.info()` from web\n * builds without branching.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface LogAPI {\n debug: (message: string) => Promise<void>\n info: (message: string) => Promise<void>\n warn: (message: string) => Promise<void>\n error: (message: string) => Promise<void>\n}\n\nexport const log: LogAPI = {\n async debug(m) { if (hasBridge('log')) await window.craft!.log.debug(m); else console.debug(m) },\n async info(m) { if (hasBridge('log')) await window.craft!.log.info(m); else console.info(m) },\n async warn(m) { if (hasBridge('log')) await window.craft!.log.warn(m); else console.warn(m) },\n async error(m) { if (hasBridge('log')) await window.craft!.log.error(m); else console.error(m) },\n}\n",
34
34
  "/**\n * Application Menu (macOS top-of-screen menubar) + Dock Menu\n *\n * Build the native menubar (`File`, `Edit`, `View`, ...) and the\n * dock-icon contextual menu. When running outside a Craft window this\n * module is a no-op — there's no portable web equivalent to the\n * macOS menubar.\n *\n * Each menu item is identified by an `id`. The native side fires a\n * `craft:menu:action` event with `{id}` when the user picks it; use\n * `menu.onAction(cb)` to listen.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface MenuItem {\n /** Stable identifier. The same id is reported by `onAction`. */\n id: string\n /** Visible label. */\n label?: string\n /**\n * Apple's stock menu roles (e.g. `'copy'`, `'paste'`, `'close'`,\n * `'quit'`). When set, AppKit hooks the system behaviour for free\n * — you don't need to reimplement Copy/Paste yourself.\n */\n role?: string\n /** Keyboard accelerator (e.g. `'Cmd+S'`). */\n accelerator?: string\n /** True for a separator line. Other fields ignored. */\n separator?: boolean\n /** Render as a checkbox; toggle via `menu.checkItem(id)`. */\n checkable?: boolean\n /** Initial checked state for checkable items. */\n checked?: boolean\n /** Disabled at startup (still renders, can't be picked). */\n disabled?: boolean\n /** Submenu under this item. */\n submenu?: MenuItem[]\n /** SF Symbol or asset name (macOS only). */\n icon?: string\n}\n\nexport interface MenuActionEvent {\n id: string\n}\n\nexport interface MenuAPI {\n /** Set the application menu (replaces the entire menubar). */\n set: (items: MenuItem[]) => Promise<void>\n /** Set the dock-icon contextual menu. */\n setDock: (items: MenuItem[]) => Promise<void>\n /** Append an item under the parent id (or top-level if parent is \"\" or \"menubar\"). */\n addItem: (parent: string, item: MenuItem) => Promise<void>\n removeItem: (id: string) => Promise<void>\n enableItem: (id: string) => Promise<void>\n disableItem: (id: string) => Promise<void>\n checkItem: (id: string) => Promise<void>\n uncheckItem: (id: string) => Promise<void>\n setItemLabel: (id: string, label: string) => Promise<void>\n /** Clear the dock-icon contextual menu. */\n clearDock: () => Promise<void>\n /** Subscribe to \"user picked a menu item\" events. */\n onAction: (cb: (event: MenuActionEvent) => void) => () => void\n}\n\nexport const menu: MenuAPI = {\n async set(items) {\n if (hasBridge('menu')) await window.craft!.menu.set(items)\n },\n async setDock(items) {\n if (hasBridge('menu')) await window.craft!.menu.setDock(items)\n },\n async addItem(parent, item) {\n if (hasBridge('menu')) await window.craft!.menu.addItem(parent, item)\n },\n async removeItem(id) { if (hasBridge('menu')) await window.craft!.menu.removeItem(id) },\n async enableItem(id) { if (hasBridge('menu')) await window.craft!.menu.enableItem(id) },\n async disableItem(id) { if (hasBridge('menu')) await window.craft!.menu.disableItem(id) },\n async checkItem(id) { if (hasBridge('menu')) await window.craft!.menu.checkItem(id) },\n async uncheckItem(id) { if (hasBridge('menu')) await window.craft!.menu.uncheckItem(id) },\n async setItemLabel(id, lbl) { if (hasBridge('menu')) await window.craft!.menu.setItemLabel(id, lbl) },\n async clearDock() { if (hasBridge('menu')) await window.craft!.menu.clearDock() },\n onAction(cb) {\n return onCraftEvent<MenuActionEvent>('craft:menu:action', cb)\n },\n}\n",
35
35
  "/**\n * CoreMIDI bindings — list endpoints, send and receive MIDI messages.\n *\n * `listSources()` returns inputs (other apps + hardware sending TO us);\n * `listDestinations()` returns outputs we can send TO. Each entry has\n * a stable `index` for the lifetime of the process — apps pass it back\n * to `send` / `subscribe`.\n *\n * Messages flow as raw bytes (`Uint8Array` / `number[]`); apps that\n * want note-on / note-off / CC parsing can layer that on top.\n *\n * Browser fallback: the Web MIDI API (`navigator.requestMIDIAccess`)\n * exposes the same conceptual surface but a different shape; this\n * module doesn't try to bridge them — apps targeting both should\n * detect and route per-environment.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface MIDIEndpoint {\n /** Stable index for the lifetime of the process. */\n index: number\n name: string\n}\n\nexport interface MIDIMessageEvent {\n /** Source index the message came from. */\n index: number\n /** Raw MIDI bytes (status + 1–2 data bytes for channel messages). */\n data: number[]\n}\n\nexport interface MIDIAPI {\n listSources: () => Promise<MIDIEndpoint[]>\n listDestinations: () => Promise<MIDIEndpoint[]>\n send: (destinationIndex: number, data: Uint8Array | number[]) => Promise<{ ok: boolean, reason?: string }>\n subscribe: (sourceIndex: number) => Promise<{ ok: boolean, reason?: string }>\n unsubscribe: (sourceIndex: number) => Promise<{ ok: boolean }>\n onMessage: (cb: (event: MIDIMessageEvent) => void) => () => void\n}\n\nexport const midi: MIDIAPI = {\n async listSources() {\n if (!hasBridge('midi')) return []\n return await window.craft!.midi.listSources()\n },\n async listDestinations() {\n if (!hasBridge('midi')) return []\n return await window.craft!.midi.listDestinations()\n },\n async send(destinationIndex, data) {\n if (!hasBridge('midi')) return { ok: false, reason: 'bridge unavailable' }\n return await window.craft!.midi.send(destinationIndex, data)\n },\n async subscribe(sourceIndex) {\n if (!hasBridge('midi')) return { ok: false, reason: 'bridge unavailable' }\n return await window.craft!.midi.subscribe(sourceIndex)\n },\n async unsubscribe(sourceIndex) {\n if (!hasBridge('midi')) return { ok: false }\n return await window.craft!.midi.unsubscribe(sourceIndex)\n },\n onMessage(cb) { return onCraftEvent<MIDIMessageEvent>('craft:midi:message', cb) },\n}\n",
36
36
  "import type { ModalButton, ModalOptions, ModalResult } from './types'\n\n/**\n * Modal Dialog Implementation\n *\n * Provides cross-platform modal dialog functionality.\n * Uses native dialogs when available, falls back to web-based modals.\n *\n * Features:\n * - Multiple dialog types (info, warning, error, success, question)\n * - Custom buttons with actions\n * - Promise-based API\n * - Keyboard support (Enter/Escape)\n * - Accessible by default\n */\n\n// =============================================================================\n// Types\n// =============================================================================\n\ninterface ModalState {\n id: string\n options: ModalOptions\n resolve: (result: ModalResult) => void\n element?: HTMLElement\n}\n\n// =============================================================================\n// Platform Detection\n// =============================================================================\n\n/**\n * Check if native dialog APIs are available\n */\nfunction hasNativeDialogSupport(): boolean {\n // Future: Check for @stacksjs/zyte bindings\n return false\n}\n\n/**\n * Check if running in browser environment\n */\nfunction isBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\n// =============================================================================\n// Modal Manager\n// =============================================================================\n\n// Active modal stack\nconst activeModals: ModalState[] = []\n\n/**\n * Generate a unique modal ID\n */\nfunction generateModalId(): string {\n return `modal-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`\n}\n\n/**\n * Get icon for modal type\n */\nfunction getModalIcon(type: ModalOptions['type']): string {\n switch (type) {\n case 'info':\n return '&#x2139;'\n case 'warning':\n return '&#x26A0;'\n case 'error':\n return '&#x2716;'\n case 'success':\n return '&#x2714;'\n case 'question':\n return '&#x2753;'\n default:\n return '&#x2139;'\n }\n}\n\n/**\n * Get default buttons for modal type\n */\nfunction getDefaultButtons(type: ModalOptions['type']): ModalButton[] {\n if (type === 'question') {\n return [\n { label: 'No', style: 'default' },\n { label: 'Yes', style: 'primary' },\n ]\n }\n return [{ label: 'OK', style: 'primary' }]\n}\n\n/**\n * Create modal HTML for web-based implementation\n */\nfunction createModalHTML(state: ModalState): string {\n const { options } = state\n const icon = getModalIcon(options.type)\n const buttons = options.buttons || getDefaultButtons(options.type)\n const typeClass = options.type || 'info'\n\n let buttonsHtml = ''\n buttons.forEach((btn, index) => {\n const styleClass = btn.style === 'destructive' ? 'destructive' : btn.style === 'primary' ? 'primary' : 'default'\n const autoFocus = index === (options.defaultButton ?? buttons.length - 1) ? 'autofocus' : ''\n buttonsHtml += `<button class=\"stx-modal-btn ${styleClass}\" data-index=\"${index}\" ${autoFocus}>${btn.label}</button>`\n })\n\n return `\n <div class=\"stx-modal-overlay\" data-modal-id=\"${state.id}\">\n <div class=\"stx-modal ${typeClass}\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"${state.id}-title\">\n <div class=\"stx-modal-icon\">${icon}</div>\n <div class=\"stx-modal-content\">\n ${options.title ? `<h2 id=\"${state.id}-title\" class=\"stx-modal-title\">${escapeHtml(options.title)}</h2>` : ''}\n <p class=\"stx-modal-message\">${escapeHtml(options.message)}</p>\n </div>\n <div class=\"stx-modal-buttons\">${buttonsHtml}</div>\n </div>\n </div>\n `\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#039;')\n}\n\n/**\n * Close a modal and resolve with result\n */\nfunction closeModal(state: ModalState, buttonIndex: number, cancelled: boolean = false): void {\n // Remove from active modals\n const index = activeModals.indexOf(state)\n if (index > -1) {\n activeModals.splice(index, 1)\n }\n\n // Remove DOM element if exists\n if (state.element && isBrowser()) {\n state.element.remove()\n }\n\n // Call button action if provided\n const buttons = state.options.buttons || getDefaultButtons(state.options.type)\n const button = buttons[buttonIndex]\n if (button?.action) {\n button.action()\n }\n\n // Resolve promise\n state.resolve({ buttonIndex, cancelled })\n}\n\n/**\n * Show a native modal dialog\n *\n * @param options - Modal configuration options\n * @returns Promise resolving to the modal result\n *\n * @example\n * ```typescript\n * const result = await showModal({\n * title: 'Confirm Action',\n * message: 'Are you sure you want to proceed?',\n * type: 'question',\n * buttons: [\n * { label: 'Cancel', style: 'default' },\n * { label: 'Confirm', style: 'primary' },\n * ],\n * })\n *\n * if (result.buttonIndex === 1) {\n * // User clicked Confirm\n * }\n * ```\n */\nexport async function showModal(options: ModalOptions): Promise<ModalResult> {\n const hasNative = hasNativeDialogSupport()\n const id = generateModalId()\n\n return new Promise((resolve) => {\n const state: ModalState = {\n id,\n options,\n resolve,\n }\n\n activeModals.push(state)\n\n if (hasNative) {\n // Future: Use native dialog\n console.log(`[stx-modal] Showing native modal: ${options.title || 'Modal'}`)\n // For now, immediately resolve\n setTimeout(() => {\n closeModal(state, options.defaultButton ?? 0, false)\n }, 0)\n }\n else if (isBrowser()) {\n // Web-based implementation\n try {\n const container = document.createElement('div')\n container.innerHTML = createModalHTML(state)\n const overlay = container.firstElementChild as HTMLElement\n\n if (!overlay) {\n // Fallback to console if DOM manipulation fails\n console.log(`[stx-modal] ${options.type?.toUpperCase() || 'INFO'}: ${options.title || 'Modal'}`)\n console.log(`[stx-modal] ${options.message}`)\n setTimeout(() => {\n closeModal(state, options.defaultButton ?? 0, false)\n }, 0)\n return\n }\n\n state.element = overlay\n\n try {\n document.body.appendChild(overlay)\n }\n catch {\n // Fallback to console if appendChild fails (e.g., in very-happy-dom)\n console.log(`[stx-modal] ${options.type?.toUpperCase() || 'INFO'}: ${options.title || 'Modal'}`)\n console.log(`[stx-modal] ${options.message}`)\n setTimeout(() => {\n closeModal(state, options.defaultButton ?? 0, false)\n }, 0)\n return\n }\n\n // Handle button clicks\n try {\n overlay.querySelectorAll('.stx-modal-btn').forEach((btn) => {\n btn.addEventListener('click', () => {\n const index = Number.parseInt((btn as HTMLElement).dataset.index || '0', 10)\n closeModal(state, index, false)\n })\n })\n }\n catch {\n // Ignore querySelectorAll errors in very-happy-dom\n }\n\n // Handle overlay click (close on backdrop click)\n try {\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) {\n const cancelIndex = options.cancelButton ?? 0\n closeModal(state, cancelIndex, true)\n }\n })\n }\n catch {\n // Ignore addEventListener errors\n }\n\n // Handle keyboard\n try {\n const handleKeydown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n const cancelIndex = options.cancelButton ?? 0\n closeModal(state, cancelIndex, true)\n document.removeEventListener('keydown', handleKeydown)\n }\n else if (e.key === 'Enter') {\n const defaultIndex = options.defaultButton ?? ((options.buttons || getDefaultButtons(options.type)).length - 1)\n closeModal(state, defaultIndex, false)\n document.removeEventListener('keydown', handleKeydown)\n }\n }\n document.addEventListener('keydown', handleKeydown)\n }\n catch {\n // Ignore keyboard event errors\n }\n\n // Focus first button\n try {\n const firstButton = overlay.querySelector('.stx-modal-btn[autofocus]') as HTMLElement\n if (firstButton) {\n firstButton.focus()\n }\n }\n catch {\n // Ignore focus errors\n }\n\n // In test environments (no real user to click) auto-resolve with the\n // default button so tests asserting ModalResult shape don't hang.\n if (typeof process !== 'undefined' && (process.env.NODE_ENV === 'test' || process.env.BUN_TEST)) {\n setTimeout(() => {\n closeModal(state, options.defaultButton ?? 0, false)\n }, 0)\n }\n }\n catch {\n // Fallback to console if any DOM operation fails\n console.log(`[stx-modal] ${options.type?.toUpperCase() || 'INFO'}: ${options.title || 'Modal'}`)\n console.log(`[stx-modal] ${options.message}`)\n setTimeout(() => {\n closeModal(state, options.defaultButton ?? 0, false)\n }, 0)\n }\n }\n else {\n // Node.js environment - console fallback\n console.log(`[stx-modal] ${options.type?.toUpperCase() || 'INFO'}: ${options.title || 'Modal'}`)\n console.log(`[stx-modal] ${options.message}`)\n const buttons = options.buttons || getDefaultButtons(options.type)\n console.log(`[stx-modal] Buttons: ${buttons.map(b => b.label).join(', ')}`)\n\n // Auto-resolve with default button\n setTimeout(() => {\n closeModal(state, options.defaultButton ?? 0, false)\n }, 0)\n }\n })\n}\n\n/**\n * Show an info modal\n */\nexport async function showInfoModal(title: string, message: string): Promise<ModalResult> {\n return showModal({ title, message, type: 'info' })\n}\n\n/**\n * Show a warning modal\n */\nexport async function showWarningModal(title: string, message: string): Promise<ModalResult> {\n return showModal({ title, message, type: 'warning' })\n}\n\n/**\n * Show an error modal\n */\nexport async function showErrorModal(title: string, message: string): Promise<ModalResult> {\n return showModal({ title, message, type: 'error' })\n}\n\n/**\n * Show a success modal\n */\nexport async function showSuccessModal(title: string, message: string): Promise<ModalResult> {\n return showModal({ title, message, type: 'success' })\n}\n\n/**\n * Show a question/confirmation modal\n */\nexport async function showQuestionModal(title: string, message: string): Promise<ModalResult> {\n return showModal({\n title,\n message,\n type: 'question',\n buttons: [\n { label: 'No', style: 'default' },\n { label: 'Yes', style: 'primary' },\n ],\n defaultButton: 1,\n cancelButton: 0,\n })\n}\n\n/**\n * Show a confirm dialog (alias for showQuestionModal)\n */\nexport async function confirm(message: string, title: string = 'Confirm'): Promise<boolean> {\n const result = await showQuestionModal(title, message)\n return result.buttonIndex === 1\n}\n\n/**\n * Show an alert dialog (single OK button)\n */\nexport async function alert(message: string, title: string = 'Alert'): Promise<void> {\n await showInfoModal(title, message)\n}\n\n/**\n * Show a prompt dialog with input field\n */\nexport async function prompt(message: string, defaultValue: string = '', title: string = 'Input'): Promise<string | null> {\n // This would need custom implementation for input field\n // For now, use console in Node.js or native prompt in browser\n if (isBrowser() && typeof window.prompt === 'function') {\n return window.prompt(message, defaultValue)\n }\n\n console.log(`[stx-modal] PROMPT: ${title}`)\n console.log(`[stx-modal] ${message}`)\n console.log(`[stx-modal] Default: ${defaultValue}`)\n\n // In Node.js, would need readline or similar\n return defaultValue\n}\n\n/**\n * Get number of active modals\n */\nexport function getActiveModalCount(): number {\n return activeModals.length\n}\n\n/**\n * Close all active modals\n */\nexport function closeAllModals(): void {\n // Close in reverse order (top-most first)\n while (activeModals.length > 0) {\n const state = activeModals[activeModals.length - 1]\n closeModal(state, 0, true)\n }\n}\n\n/**\n * CSS styles for web-based modals\n */\nexport const MODAL_STYLES = `\n.stx-modal-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n animation: stx-modal-fade-in 0.15s ease-out;\n}\n\n@keyframes stx-modal-fade-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.stx-modal {\n background: #fff;\n border-radius: 12px;\n padding: 24px;\n max-width: 400px;\n width: 90%;\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);\n animation: stx-modal-slide-up 0.2s ease-out;\n}\n\n@keyframes stx-modal-slide-up {\n from { transform: translateY(20px); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n}\n\n@media (prefers-color-scheme: dark) {\n .stx-modal {\n background: #2d2d2d;\n color: #fff;\n }\n}\n\n.stx-modal-icon {\n font-size: 48px;\n text-align: center;\n margin-bottom: 16px;\n}\n\n.stx-modal.info .stx-modal-icon { color: #3498db; }\n.stx-modal.warning .stx-modal-icon { color: #f39c12; }\n.stx-modal.error .stx-modal-icon { color: #e74c3c; }\n.stx-modal.success .stx-modal-icon { color: #27ae60; }\n.stx-modal.question .stx-modal-icon { color: #9b59b6; }\n\n.stx-modal-content {\n text-align: center;\n margin-bottom: 24px;\n}\n\n.stx-modal-title {\n margin: 0 0 8px;\n font-size: 20px;\n font-weight: 600;\n}\n\n.stx-modal-message {\n margin: 0;\n color: #666;\n line-height: 1.5;\n}\n\n@media (prefers-color-scheme: dark) {\n .stx-modal-message { color: #aaa; }\n}\n\n.stx-modal-buttons {\n display: flex;\n gap: 8px;\n justify-content: center;\n}\n\n.stx-modal-btn {\n padding: 10px 24px;\n border-radius: 6px;\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n border: none;\n transition: background 0.15s, transform 0.1s;\n}\n\n.stx-modal-btn:hover {\n transform: translateY(-1px);\n}\n\n.stx-modal-btn:active {\n transform: translateY(0);\n}\n\n.stx-modal-btn.default {\n background: #e0e0e0;\n color: #333;\n}\n\n.stx-modal-btn.default:hover {\n background: #d0d0d0;\n}\n\n.stx-modal-btn.primary {\n background: #3498db;\n color: #fff;\n}\n\n.stx-modal-btn.primary:hover {\n background: #2980b9;\n}\n\n.stx-modal-btn.destructive {\n background: #e74c3c;\n color: #fff;\n}\n\n.stx-modal-btn.destructive:hover {\n background: #c0392b;\n}\n\n@media (prefers-color-scheme: dark) {\n .stx-modal-btn.default {\n background: #444;\n color: #fff;\n }\n .stx-modal-btn.default:hover {\n background: #555;\n }\n}\n`\n",
37
37
  "/**\n * Native Auto-Launch (start at login)\n *\n * Tells the OS to launch the app automatically when the user signs in.\n * Backed by `SMAppService` on macOS Ventura+. **Different from\n * `autolaunch.ts`** — that older module shells out to subprocesses\n * (`osascript`, etc); this one uses the modern Apple API.\n *\n * For a clean migration: prefer this module for new code. Keep\n * `autolaunch.ts` for backward compatibility with apps that haven't\n * adopted the Craft bridge yet.\n *\n * On systems where SMAppService isn't available (older macOS, Linux,\n * Windows) `enable`/`disable` resolve to `false` — let the caller\n * fall back to the legacy module.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface NativeAutoLaunchAPI {\n /** Register the app to launch at login. Resolves to true on success. */\n enable: () => Promise<boolean>\n /** Unregister. Resolves to true on success. */\n disable: () => Promise<boolean>\n /** True if currently registered. */\n isEnabled: () => Promise<boolean>\n}\n\nexport const nativeAutoLaunch: NativeAutoLaunchAPI = {\n async enable() {\n if (!hasBridge('autoLaunch')) return false\n return await window.craft!.autoLaunch.enable()\n },\n async disable() {\n if (!hasBridge('autoLaunch')) return false\n return await window.craft!.autoLaunch.disable()\n },\n async isEnabled() {\n if (!hasBridge('autoLaunch')) return false\n return await window.craft!.autoLaunch.isEnabled()\n },\n}\n",
38
- "/**\n * Network / Reachability\n *\n * Connection type, WiFi info, IP/MAC addresses, VPN status, proxy\n * settings. When running in a Craft native window this dispatches to\n * the `craft.network` bridge (SystemConfiguration on macOS / NLM on\n * Windows / NetworkManager on Linux). Browser fallback uses the\n * `navigator.connection` API where available.\n *\n * Some fields (MAC address, VPN, proxy) are unavailable in browsers\n * for security reasons — they return empty/false there rather than\n * throwing, so feature-detection on the result is the cleanest pattern.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type ConnectionType = \n| 'wifi'\n| 'ethernet'\n| 'cellular'\n| 'bluetooth'\n| 'vpn'\n| 'none'\n| 'unknown'\n\nexport interface NetworkInterface {\n name: string\n /** IPv4/IPv6 address. */\n address: string\n /** True if the interface is up and has carrier. */\n isUp: boolean\n /** True for loopback (127.0.0.1 / ::1). */\n isLoopback: boolean\n}\n\nexport interface ProxySettings {\n http?: string\n https?: string\n ftp?: string\n socks?: string\n /** Hosts that should bypass the proxy. */\n exceptions?: string[]\n}\n\nexport interface NetworkAPI {\n /** Coarse-grained type of the active connection. */\n connectionType: () => Promise<ConnectionType>\n /** SSID of the joined WiFi network, or undefined if not on WiFi. */\n wifiSSID: () => Promise<string | undefined>\n /** Signal strength in dBm (negative — closer to 0 = stronger). */\n wifiSignalStrength: () => Promise<number | undefined>\n /** Primary IP address. */\n ipAddress: () => Promise<string>\n /** Hardware address of the active interface. May be empty in browsers. */\n macAddress: () => Promise<string>\n /** Every active network interface. */\n interfaces: () => Promise<NetworkInterface[]>\n /** True if a VPN tunnel is up. False on web (always). */\n isVPNConnected: () => Promise<boolean>\n /** System proxy settings. Empty object on web. */\n proxySettings: () => Promise<ProxySettings>\n /** Open the system Network preference pane / settings page. */\n openPreferences: () => Promise<void>\n /** Subscribe to reachability changes. */\n onChange: (cb: (info: { type: ConnectionType, online: boolean }) => void) => () => void\n}\n\nexport const network: NetworkAPI = {\n async connectionType() {\n if (hasBridge('network')) return await window.craft!.network.connectionType()\n return webConnectionType()\n },\n async wifiSSID() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSSID()\n return v || undefined\n }\n return undefined\n },\n async wifiSignalStrength() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSignalStrength()\n return typeof v === 'number' ? v : undefined\n }\n return undefined\n },\n async ipAddress() {\n if (hasBridge('network')) return await window.craft!.network.ipAddress()\n return ''\n },\n async macAddress() {\n if (hasBridge('network')) return await window.craft!.network.macAddress()\n return ''\n },\n async interfaces() {\n if (hasBridge('network')) return await window.craft!.network.interfaces()\n return []\n },\n async isVPNConnected() {\n if (hasBridge('network')) return await window.craft!.network.isVPNConnected()\n return false\n },\n async proxySettings() {\n if (hasBridge('network')) {\n const r = await window.craft!.network.proxySettings()\n return r || {}\n }\n return {}\n },\n async openPreferences() {\n if (hasBridge('network')) await window.craft!.network.openPreferences()\n },\n onChange(cb): () => void {\n if (hasBridge('network')) {\n return onCraftEvent<{ type: ConnectionType, online: boolean }>('craft:networkChange', cb)\n }\n if (typeof window === 'undefined') return () => {}\n const onlineH = () => cb({ type: webConnectionType(), online: true })\n const offlineH = () => cb({ type: 'none', online: false })\n window.addEventListener('online', onlineH)\n window.addEventListener('offline', offlineH)\n return () => {\n window.removeEventListener('online', onlineH)\n window.removeEventListener('offline', offlineH)\n }\n },\n}\n\nfunction webConnectionType(): ConnectionType {\n if (typeof navigator === 'undefined') return 'unknown'\n if (navigator.onLine === false) return 'none'\n const conn = (navigator as any).connection\n if (!conn) return 'unknown'\n // navigator.connection.type: 'wifi'|'cellular'|'ethernet'|'bluetooth'|'wimax'|'none'|'other'|'unknown'\n const t = String(conn.type || conn.effectiveType || 'unknown').toLowerCase()\n if (t === 'wifi' || t === 'cellular' || t === 'ethernet' || t === 'bluetooth' || t === 'none') return t\n return 'unknown'\n}\n",
38
+ "/**\n * Network / Reachability\n *\n * Connection type, WiFi info, IP/MAC addresses, VPN status, proxy\n * settings. When running in a Craft native window this dispatches to\n * the `craft.network` bridge (SystemConfiguration on macOS / NLM on\n * Windows / NetworkManager on Linux). Browser fallback uses the\n * `navigator.connection` API where available.\n *\n * Some fields (MAC address, VPN, proxy) are unavailable in browsers\n * for security reasons — they return empty/false there rather than\n * throwing, so feature-detection on the result is the cleanest pattern.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type ConnectionType =\n| 'wifi'\n| 'ethernet'\n| 'cellular'\n| 'bluetooth'\n| 'vpn'\n| 'none'\n| 'unknown'\n\nexport interface NetworkInterface {\n name: string\n /** IPv4/IPv6 address. */\n address: string\n /** True if the interface is up and has carrier. */\n isUp: boolean\n /** True for loopback (127.0.0.1 / ::1). */\n isLoopback: boolean\n}\n\nexport interface ProxySettings {\n http?: string\n https?: string\n ftp?: string\n socks?: string\n /** Hosts that should bypass the proxy. */\n exceptions?: string[]\n}\n\nexport interface NetworkAPI {\n /** Coarse-grained type of the active connection. */\n connectionType: () => Promise<ConnectionType>\n /** SSID of the joined WiFi network, or undefined if not on WiFi. */\n wifiSSID: () => Promise<string | undefined>\n /** Signal strength in dBm (negative — closer to 0 = stronger). */\n wifiSignalStrength: () => Promise<number | undefined>\n /** Primary IP address. */\n ipAddress: () => Promise<string>\n /** Hardware address of the active interface. May be empty in browsers. */\n macAddress: () => Promise<string>\n /** Every active network interface. */\n interfaces: () => Promise<NetworkInterface[]>\n /** True if a VPN tunnel is up. False on web (always). */\n isVPNConnected: () => Promise<boolean>\n /** System proxy settings. Empty object on web. */\n proxySettings: () => Promise<ProxySettings>\n /** Open the system Network preference pane / settings page. */\n openPreferences: () => Promise<void>\n /** Subscribe to reachability changes. */\n onChange: (cb: (info: { type: ConnectionType, online: boolean }) => void) => () => void\n}\n\nexport const network: NetworkAPI = {\n async connectionType() {\n if (hasBridge('network')) return await window.craft!.network.connectionType()\n return webConnectionType()\n },\n async wifiSSID() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSSID()\n return v || undefined\n }\n return undefined\n },\n async wifiSignalStrength() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSignalStrength()\n return typeof v === 'number' ? v : undefined\n }\n return undefined\n },\n async ipAddress() {\n if (hasBridge('network')) return await window.craft!.network.ipAddress()\n return ''\n },\n async macAddress() {\n if (hasBridge('network')) return await window.craft!.network.macAddress()\n return ''\n },\n async interfaces() {\n if (hasBridge('network')) return await window.craft!.network.interfaces()\n return []\n },\n async isVPNConnected() {\n if (hasBridge('network')) return await window.craft!.network.isVPNConnected()\n return false\n },\n async proxySettings() {\n if (hasBridge('network')) {\n const r = await window.craft!.network.proxySettings()\n return r || {}\n }\n return {}\n },\n async openPreferences() {\n if (hasBridge('network')) await window.craft!.network.openPreferences()\n },\n onChange(cb): () => void {\n if (hasBridge('network')) {\n return onCraftEvent<{ type: ConnectionType, online: boolean }>('craft:networkChange', cb)\n }\n if (typeof window === 'undefined') return () => {}\n const onlineH = () => cb({ type: webConnectionType(), online: true })\n const offlineH = () => cb({ type: 'none', online: false })\n window.addEventListener('online', onlineH)\n window.addEventListener('offline', offlineH)\n return () => {\n window.removeEventListener('online', onlineH)\n window.removeEventListener('offline', offlineH)\n }\n },\n}\n\nfunction webConnectionType(): ConnectionType {\n if (typeof navigator === 'undefined') return 'unknown'\n if (navigator.onLine === false) return 'none'\n const conn = (navigator as any).connection\n if (!conn) return 'unknown'\n // navigator.connection.type: 'wifi'|'cellular'|'ethernet'|'bluetooth'|'wimax'|'none'|'other'|'unknown'\n const t = String(conn.type || conn.effectiveType || 'unknown').toLowerCase()\n if (t === 'wifi' || t === 'cellular' || t === 'ethernet' || t === 'bluetooth' || t === 'none') return t\n return 'unknown'\n}\n",
39
39
  "/**\n * System Notifications\n *\n * Send banner notifications and update the dock badge. When running\n * inside a Craft native window, uses UNUserNotificationCenter (macOS)\n * / D-Bus org.freedesktop.Notifications (Linux) / Toast (Windows).\n * In a browser, falls back to the standard `Notification` web API.\n *\n * **NOT** the same as `alerts.ts` — that module displays in-app toasts\n * (DOM overlays). This module dispatches to the OS notification center,\n * which persists in Notification Center / Action Center / etc.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface NotificationAttachment {\n /** Stable id within the notification. Optional — UNNotificationAttachment auto-generates one when omitted. */\n id?: string\n /** Local file URL or absolute path. UNNotificationAttachment requires a file URL. */\n url: string\n /**\n * MIME-ish hint. UNNotificationAttachment infers from the file\n * extension when absent; pass when the extension is missing or\n * ambiguous.\n */\n type?: 'image' | 'audio' | 'video' | string\n}\n\nexport interface NotificationAction {\n /** Stable id sent back via `onActionClicked`. */\n id: string\n /** Visible label on the action button. */\n title: string\n /**\n * UNNotificationActionOptions — `'destructive'` shows the action in\n * red (delete-style), `'foreground'` brings the app to the foreground\n * after handling. Default is the standard non-destructive background\n * action.\n */\n style?: 'default' | 'destructive' | 'foreground'\n /** When present, hide the action behind FaceID / Touch ID before firing. */\n authenticationRequired?: boolean\n}\n\nexport interface NotificationOptions {\n /** Required title shown in bold at the top. */\n title: string\n /** Body text — may wrap. */\n body?: string\n /** Subtitle (macOS only — second line above body). */\n subtitle?: string\n /** Stable identifier so callers can `cancel(id)` later. */\n id?: string\n /** ISO timestamp / Date / epoch ms. Defaults to \"now\". */\n triggerAt?: string | number | Date\n /** Sound to play. `'default'` plays the system sound. */\n sound?: 'default' | 'silent' | string\n /** Icon URL (web fallback only — native uses the app icon). */\n icon?: string\n /** Number badge to display on the app icon. */\n badge?: number\n /** Custom data passed back to your `onClick` handler. */\n data?: unknown\n /**\n * Image / audio / video attachments shown alongside the notification\n * (UNNotificationAttachment on macOS, image-payload on Linux/Windows\n * where supported). Each entry needs a file URL or absolute path that\n * the OS can read.\n */\n attachments?: NotificationAttachment[]\n /**\n * Custom buttons added to the notification. macOS lets you ship up to\n * four; users see \"More\" if more are present. The `id` you set here\n * comes back via `onActionClicked` when the user taps the action.\n */\n actions?: NotificationAction[]\n /**\n * Inline reply text-field (macOS only). When set, the notification\n * shows a reply field; the user's text comes back via `onReply`.\n */\n reply?: {\n /** Placeholder text inside the reply box. */\n placeholder?: string\n /** Submit button label. Defaults to the system's \"Send\". */\n sendButtonTitle?: string\n }\n /**\n * Category identifier — pre-registered via UNUserNotificationCenter\n * before sending. When present, the notification reuses the actions\n * registered against that category instead of repeating them inline.\n */\n categoryId?: string\n /**\n * Replace any earlier notifications with the same thread id (groups\n * them in macOS's notification center; behaves as a \"bucket\" elsewhere).\n */\n threadId?: string\n}\n\nexport interface NotificationActionEvent {\n /** Notification id (`options.id`). */\n notificationId?: string\n /** Action id (matches `NotificationAction.id`). */\n actionId: string\n /** When `actionId` is the system-supplied \"default\" tap action. */\n isDefault?: boolean\n}\n\nexport interface NotificationReplyEvent {\n notificationId?: string\n /** Text the user typed into the inline reply field. */\n text: string\n}\n\nexport interface NotificationCategory {\n /** Stable id referenced via `NotificationOptions.categoryId`. */\n id: string\n /** Default actions attached to every notification of this category. */\n actions: NotificationAction[]\n}\n\nexport interface SystemNotifications {\n /** Show a notification immediately. */\n show: (options: NotificationOptions) => Promise<void>\n /** Schedule a future notification. Use `triggerAt` to set the time. */\n schedule: (options: NotificationOptions) => Promise<void>\n /** Cancel a scheduled or visible notification by id. */\n cancel: (id: string) => Promise<void>\n /** Cancel everything we've scheduled or shown. */\n cancelAll: () => Promise<void>\n /** Set the dock/taskbar badge count. */\n setBadge: (n: number) => Promise<void>\n /** Clear the dock/taskbar badge. */\n clearBadge: () => Promise<void>\n /**\n * Ask the user for notification permission.\n * Returns `true` if granted (or already granted).\n */\n requestPermission: () => Promise<boolean>\n /**\n * Pre-register a category — same shape as `actions` on a single\n * notification, but reusable across many. Apps generally do this once\n * at boot so the notification center has the actions ready before any\n * notification fires.\n */\n registerCategories: (categories: NotificationCategory[]) => Promise<void>\n /**\n * Subscribe to user taps on actions (including the default tap on the\n * notification body — `event.isDefault === true` for that case).\n */\n onActionClicked: (cb: (event: NotificationActionEvent) => void) => () => void\n /**\n * Subscribe to inline-reply submissions. Only fires for notifications\n * that opted into `reply`.\n */\n onReply: (cb: (event: NotificationReplyEvent) => void) => () => void\n}\n\nexport const notifications: SystemNotifications = {\n async show(options: NotificationOptions): Promise<void> {\n if (!options.title) throw new Error('notification title is required')\n if (hasBridge('notifications')) {\n await window.craft!.notifications.show(options)\n return\n }\n // Web fallback: standard Notification API.\n if (typeof window !== 'undefined' && 'Notification' in window) {\n const N = (window as any).Notification\n if (N.permission === 'granted') {\n new N(options.title, { body: options.body, icon: options.icon })\n }\n else if (N.permission === 'default') {\n const granted = (await N.requestPermission()) === 'granted'\n if (granted) new N(options.title, { body: options.body, icon: options.icon })\n }\n }\n },\n\n async schedule(options: NotificationOptions): Promise<void> {\n if (hasBridge('notifications')) {\n // The native side accepts a flat object. Normalize trigger to ISO so\n // the parser on the Zig side doesn't have to deal with Date objects.\n const o = { ...options }\n if (o.triggerAt instanceof Date) o.triggerAt = o.triggerAt.toISOString()\n await window.craft!.notifications.schedule(o)\n return\n }\n // Web has no scheduling primitive — fall back to setTimeout.\n const fireAt = toEpochMs(options.triggerAt)\n const delay = Math.max(0, fireAt - Date.now())\n setTimeout(() => { this.show(options).catch(() => {}) }, delay)\n },\n\n async cancel(id: string): Promise<void> {\n if (hasBridge('notifications')) {\n await window.craft!.notifications.cancel(id)\n }\n // Web has no per-id cancellation; nothing to do.\n },\n\n async cancelAll(): Promise<void> {\n if (hasBridge('notifications')) {\n await window.craft!.notifications.cancelAll()\n }\n },\n\n async setBadge(n: number): Promise<void> {\n // Negative counts cause native-side display glitches (Apple\n // documents non-negative). Clamp at the JS boundary so apps don't\n // have to remember the constraint, and round so a fractional value\n // (e.g. from a divide) doesn't render as \"5.5\".\n const safe = Math.max(0, Math.round(Number.isFinite(n) ? n : 0))\n if (hasBridge('notifications')) {\n await window.craft!.notifications.setBadge(safe)\n return\n }\n // Some browsers expose navigator.setAppBadge (PWA Badging API).\n if (typeof navigator !== 'undefined' && (navigator as any).setAppBadge) {\n try { await (navigator as any).setAppBadge(safe) } catch { /* ignore */ }\n }\n },\n\n async clearBadge(): Promise<void> {\n if (hasBridge('notifications')) {\n await window.craft!.notifications.clearBadge()\n return\n }\n if (typeof navigator !== 'undefined' && (navigator as any).clearAppBadge) {\n try { await (navigator as any).clearAppBadge() } catch { /* ignore */ }\n }\n },\n\n async requestPermission(): Promise<boolean> {\n if (hasBridge('notifications')) {\n return await window.craft!.notifications.requestPermission()\n }\n if (typeof window !== 'undefined' && 'Notification' in window) {\n const N = (window as any).Notification\n if (N.permission === 'granted') return true\n if (N.permission === 'denied') return false\n const result = await N.requestPermission()\n return result === 'granted'\n }\n return false\n },\n\n async registerCategories(categories) {\n if (!Array.isArray(categories) || categories.length === 0) return\n if (hasBridge('notifications')) {\n const fn = window.craft!.notifications.registerCategories\n if (typeof fn === 'function') await fn(categories)\n }\n // No web fallback — the standard Notification API has no equivalent.\n },\n\n onActionClicked(cb) { return onCraftEvent<NotificationActionEvent>('craft:notification:actionClicked', cb) },\n onReply(cb) { return onCraftEvent<NotificationReplyEvent>('craft:notification:reply', cb) },\n}\n\nfunction toEpochMs(t: string | number | Date | undefined): number {\n if (t == null) return Date.now()\n if (t instanceof Date) return t.getTime()\n if (typeof t === 'number') return t\n const parsed = Date.parse(t)\n return Number.isNaN(parsed) ? Date.now() : parsed\n}\n",
40
40
  "/**\n * PDF reader (PDFKit on macOS).\n *\n * Two operations covering the common \"read the file and grep its\n * text\" use case:\n *\n * - `countPages(path)` — total page count\n * - `extractText(path)` — concatenated plaintext of all pages\n *\n * Apps that need richer extraction (per-page text, embedded images,\n * annotations, form-field values) should walk the document directly\n * via the lower-level PDFKit surface.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface PDFAPI {\n /** Number of pages in the PDF, or 0 on error. */\n countPages: (path: string) => Promise<number>\n /** Concatenated plain text of all pages. Empty string on error. */\n extractText: (path: string) => Promise<string>\n}\n\nexport const pdf: PDFAPI = {\n async countPages(path) {\n if (!path) throw new Error('pdf.countPages: path is required')\n if (!hasBridge('pdf')) return 0\n return await window.craft!.pdf.countPages(path)\n },\n async extractText(path) {\n if (!path) throw new Error('pdf.extractText: path is required')\n if (!hasBridge('pdf')) return ''\n return await window.craft!.pdf.extractText(path)\n },\n}\n",
41
- "/**\n * Privacy Permissions\n *\n * Check and request OS-level permission for sensitive capabilities\n * (camera, microphone, screen recording, etc). Wraps macOS TCC\n * (`AVCaptureDevice authorizationStatusForMediaType:`) and equivalents.\n *\n * Browser fallback uses the (limited) `navigator.permissions.query`\n * API where available — note that the web API only knows about a small\n * subset of names ('camera', 'microphone', 'geolocation', 'notifications').\n */\nimport { hasBridge } from './_bridge'\n\n/** Status values match the macOS TCC convention. */\nexport type PermissionStatus = 'granted' | 'denied' | 'restricted' | 'undetermined' | 'not-supported'\n\nexport type PermissionName = \n| 'camera'\n| 'microphone'\n| 'screen_recording'\n| 'accessibility'\n| 'full_disk_access'\n| 'input_monitoring'\n| 'location'\n| 'notifications'\n| 'contacts'\n| 'calendar'\n| 'reminders'\n| 'photos'\n| 'bluetooth'\n\nexport interface PermissionsAPI {\n /** Read current status without prompting. */\n check: (name: PermissionName) => Promise<PermissionStatus>\n /**\n * Request permission. On macOS this triggers the system modal for\n * permissions that haven't been answered yet; for ones already in\n * a non-undetermined state, the user has to flip it manually in\n * System Settings — call `openSettings(name)` to jump them there.\n */\n request: (name: PermissionName) => Promise<PermissionStatus>\n /** Open the Privacy pane scoped to the named permission. */\n openSettings: (name?: PermissionName) => Promise<void>\n}\n\nexport const permissions: PermissionsAPI = {\n async check(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.check(name)\n return await webCheck(name)\n },\n async request(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.request(name)\n return await webRequest(name)\n },\n async openSettings(name) {\n if (hasBridge('permissions')) await window.craft!.permissions.openSettings(name)\n },\n}\n\nasync function webCheck(name: PermissionName): Promise<PermissionStatus> {\n if (typeof navigator === 'undefined' || !(navigator as any).permissions?.query) return 'not-supported'\n try {\n const result = await (navigator as any).permissions.query({ name })\n return mapWebState(result.state)\n }\n catch { return 'not-supported' }\n}\n\nasync function webRequest(name: PermissionName): Promise<PermissionStatus> {\n // The web has no general-purpose `request` — most APIs prompt\n // implicitly when you try to use them. We special-case the well-known\n // ones and otherwise fall back to a `check`.\n if (name === 'notifications' && typeof window !== 'undefined' && 'Notification' in window) {\n const r = await (window as any).Notification.requestPermission()\n return r === 'granted' ? 'granted' : r === 'denied' ? 'denied' : 'undetermined'\n }\n return await webCheck(name)\n}\n\nfunction mapWebState(s: string): PermissionStatus {\n if (s === 'granted') return 'granted'\n if (s === 'denied') return 'denied'\n if (s === 'prompt') return 'undetermined'\n return 'undetermined'\n}\n",
41
+ "/**\n * Privacy Permissions\n *\n * Check and request OS-level permission for sensitive capabilities\n * (camera, microphone, screen recording, etc). Wraps macOS TCC\n * (`AVCaptureDevice authorizationStatusForMediaType:`) and equivalents.\n *\n * Browser fallback uses the (limited) `navigator.permissions.query`\n * API where available — note that the web API only knows about a small\n * subset of names ('camera', 'microphone', 'geolocation', 'notifications').\n */\nimport { hasBridge } from './_bridge'\n\n/** Status values match the macOS TCC convention. */\nexport type PermissionStatus = 'granted' | 'denied' | 'restricted' | 'undetermined' | 'not-supported'\n\nexport type PermissionName =\n| 'camera'\n| 'microphone'\n| 'screen_recording'\n| 'accessibility'\n| 'full_disk_access'\n| 'input_monitoring'\n| 'location'\n| 'notifications'\n| 'contacts'\n| 'calendar'\n| 'reminders'\n| 'photos'\n| 'bluetooth'\n\nexport interface PermissionsAPI {\n /** Read current status without prompting. */\n check: (name: PermissionName) => Promise<PermissionStatus>\n /**\n * Request permission. On macOS this triggers the system modal for\n * permissions that haven't been answered yet; for ones already in\n * a non-undetermined state, the user has to flip it manually in\n * System Settings — call `openSettings(name)` to jump them there.\n */\n request: (name: PermissionName) => Promise<PermissionStatus>\n /** Open the Privacy pane scoped to the named permission. */\n openSettings: (name?: PermissionName) => Promise<void>\n}\n\nexport const permissions: PermissionsAPI = {\n async check(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.check(name)\n return await webCheck(name)\n },\n async request(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.request(name)\n return await webRequest(name)\n },\n async openSettings(name) {\n if (hasBridge('permissions')) await window.craft!.permissions.openSettings(name)\n },\n}\n\nasync function webCheck(name: PermissionName): Promise<PermissionStatus> {\n if (typeof navigator === 'undefined' || !(navigator as any).permissions?.query) return 'not-supported'\n try {\n const result = await (navigator as any).permissions.query({ name })\n return mapWebState(result.state)\n }\n catch { return 'not-supported' }\n}\n\nasync function webRequest(name: PermissionName): Promise<PermissionStatus> {\n // The web has no general-purpose `request` — most APIs prompt\n // implicitly when you try to use them. We special-case the well-known\n // ones and otherwise fall back to a `check`.\n if (name === 'notifications' && typeof window !== 'undefined' && 'Notification' in window) {\n const r = await (window as any).Notification.requestPermission()\n return r === 'granted' ? 'granted' : r === 'denied' ? 'denied' : 'undetermined'\n }\n return await webCheck(name)\n}\n\nfunction mapWebState(s: string): PermissionStatus {\n if (s === 'granted') return 'granted'\n if (s === 'denied') return 'denied'\n if (s === 'prompt') return 'undetermined'\n return 'undetermined'\n}\n",
42
42
  "/**\n * Power Management API\n *\n * Provides control over system sleep behavior using macOS caffeinate.\n * Spawns and manages /usr/bin/caffeinate processes.\n *\n * @example\n * ```typescript\n * import { caffeinate, decaffeinate, isCaffeinated, getCaffeinateStatus } from '@stacksjs/desktop'\n *\n * // Prevent sleep indefinitely\n * const instance = caffeinate()\n *\n * // Prevent sleep for 30 minutes\n * const instance = caffeinate({ duration: 30 })\n *\n * // Prevent only display sleep for 1 hour\n * const instance = caffeinate({\n * duration: 60,\n * preventDisplaySleep: true,\n * preventIdleSleep: false,\n * preventSystemSleep: false,\n * })\n *\n * // Check status\n * console.log(isCaffeinated()) // true\n * console.log(getCaffeinateStatus())\n *\n * // Stop\n * decaffeinate()\n * ```\n */\nimport type { Subprocess } from 'bun'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CaffeinateOptions {\n /** Duration in minutes. -1 or undefined = indefinite */\n duration?: number\n /** Prevent display from sleeping (-d flag). Default: true */\n preventDisplaySleep?: boolean\n /** Prevent system from idle sleeping (-i flag). Default: true */\n preventIdleSleep?: boolean\n /** Prevent system from sleeping (-s flag). Default: true */\n preventSystemSleep?: boolean\n /** Prevent disk from sleeping (-m flag). Default: false */\n preventDiskSleep?: boolean\n /** Create an assertion to prevent sleep on behalf of user (-u flag). Default: true */\n assertUserActivity?: boolean\n}\n\nexport interface CaffeinateInstance {\n /** Process ID of the caffeinate process */\n readonly pid: number\n /** When caffeinate was started */\n readonly startedAt: Date\n /** When caffeinate will end (null = indefinite) */\n readonly endsAt: Date | null\n /** Options used to create this instance */\n readonly options: CaffeinateOptions\n /** Whether this instance is still active */\n readonly isActive: boolean\n /** Remaining milliseconds (null = indefinite, 0 = expired) */\n readonly remainingMs: number | null\n /** Elapsed milliseconds since start */\n readonly elapsedMs: number\n /** Stop this caffeinate instance */\n stop(): void\n /** Register a callback for when this instance expires (duration-based only) */\n onExpire(handler: () => void): void\n}\n\nexport interface CaffeinateStatus {\n /** Whether caffeinate is currently active */\n active: boolean\n /** The current caffeinate instance, if any */\n instance: CaffeinateInstance | null\n /** When the current session started */\n startedAt: Date | null\n /** When the current session will end */\n endsAt: Date | null\n /** Duration in minutes (-1 = indefinite, null = not active) */\n durationMinutes: number | null\n}\n\n// ============================================================================\n// Internal State\n// ============================================================================\n\nlet currentProcess: Subprocess | null = null\nlet currentInstance: CaffeinateInstanceImpl | null = null\n\nclass CaffeinateInstanceImpl implements CaffeinateInstance {\n private _process: Subprocess\n private _startedAt: Date\n private _endsAt: Date | null\n private _options: CaffeinateOptions\n private _expireHandlers: Array<() => void> = []\n private _expireTimer: ReturnType<typeof setTimeout> | null = null\n private _stopped = false\n\n constructor(process: Subprocess, options: CaffeinateOptions) {\n this._process = process\n this._options = options\n this._startedAt = new Date()\n\n const duration = options.duration\n if (duration && duration > 0) {\n const durationMs = duration * 60 * 1000\n this._endsAt = new Date(this._startedAt.getTime() + durationMs)\n\n this._expireTimer = setTimeout(() => {\n this._stopped = true\n for (const handler of this._expireHandlers) {\n try {\n handler()\n }\n catch {}\n }\n }, durationMs)\n }\n else {\n this._endsAt = null\n }\n }\n\n get pid(): number {\n return this._process.pid\n }\n\n get startedAt(): Date {\n return this._startedAt\n }\n\n get endsAt(): Date | null {\n return this._endsAt\n }\n\n get options(): CaffeinateOptions {\n return { ...this._options }\n }\n\n get isActive(): boolean {\n if (this._stopped)\n return false\n // Check if process is still running\n return this._process.exitCode === null\n }\n\n get remainingMs(): number | null {\n if (!this._endsAt)\n return null\n const remaining = this._endsAt.getTime() - Date.now()\n return Math.max(0, remaining)\n }\n\n get elapsedMs(): number {\n return Date.now() - this._startedAt.getTime()\n }\n\n stop(): void {\n if (this._stopped)\n return\n this._stopped = true\n\n if (this._expireTimer) {\n clearTimeout(this._expireTimer)\n this._expireTimer = null\n }\n\n try {\n this._process.kill()\n }\n catch {\n // Process may have already exited\n }\n }\n\n onExpire(handler: () => void): void {\n this._expireHandlers.push(handler)\n }\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Start preventing the system from sleeping.\n *\n * Spawns a `/usr/bin/caffeinate` process with the specified options.\n * If caffeinate is already active, the previous instance is stopped first.\n *\n * @param options - Caffeinate configuration\n * @returns A CaffeinateInstance for monitoring and controlling the session\n */\nexport function caffeinate(options: CaffeinateOptions = {}): CaffeinateInstance {\n // Stop any existing instance\n decaffeinate()\n\n const {\n duration,\n preventDisplaySleep = true,\n preventIdleSleep = true,\n preventSystemSleep = true,\n preventDiskSleep = false,\n assertUserActivity = true,\n } = options\n\n // Build flags\n const flags: string[] = []\n if (preventDisplaySleep)\n flags.push('-d')\n if (preventIdleSleep)\n flags.push('-i')\n if (preventSystemSleep)\n flags.push('-s')\n if (preventDiskSleep)\n flags.push('-m')\n if (assertUserActivity)\n flags.push('-u')\n\n // Build args\n const args: string[] = [...flags]\n\n // Add duration if specified (in seconds)\n if (duration && duration > 0) {\n args.push('-t', String(duration * 60))\n }\n\n // Spawn caffeinate\n const proc = Bun.spawn(['/usr/bin/caffeinate', ...args], {\n stdio: ['ignore', 'ignore', 'ignore'],\n })\n\n const instance = new CaffeinateInstanceImpl(proc, options)\n currentProcess = proc\n currentInstance = instance\n\n return instance\n}\n\n/**\n * Stop caffeinate and allow the system to sleep normally.\n *\n * @param instance - Specific instance to stop. If omitted, stops the current active instance.\n */\nexport function decaffeinate(instance?: CaffeinateInstance): void {\n if (instance) {\n instance.stop()\n if (currentInstance === instance) {\n currentProcess = null\n currentInstance = null\n }\n return\n }\n\n if (currentInstance) {\n currentInstance.stop()\n }\n currentProcess = null\n currentInstance = null\n}\n\n/**\n * Check if caffeinate is currently active.\n */\nexport function isCaffeinated(): boolean {\n return currentInstance !== null && currentInstance.isActive\n}\n\n/**\n * Get the current caffeinate status.\n */\nexport function getCaffeinateStatus(): CaffeinateStatus {\n if (!currentInstance || !currentInstance.isActive) {\n return {\n active: false,\n instance: null,\n startedAt: null,\n endsAt: null,\n durationMinutes: null,\n }\n }\n\n const opts = currentInstance.options\n const duration = opts.duration\n\n return {\n active: true,\n instance: currentInstance,\n startedAt: currentInstance.startedAt,\n endsAt: currentInstance.endsAt,\n durationMinutes: (duration && duration > 0) ? duration : -1,\n }\n}\n\n/**\n * Format remaining caffeinate time as a human-readable string.\n *\n * @example\n * ```typescript\n * formatRemainingTime(instance) // \"25:30\" or \"∞\"\n * ```\n */\nexport function formatRemainingTime(instance?: CaffeinateInstance | null): string {\n const inst = instance || currentInstance\n if (!inst || !inst.isActive)\n return '0:00'\n\n const remaining = inst.remainingMs\n if (remaining === null)\n return '∞'\n\n const totalSeconds = Math.ceil(remaining / 1000)\n const hours = Math.floor(totalSeconds / 3600)\n const minutes = Math.floor((totalSeconds % 3600) / 60)\n const seconds = totalSeconds % 60\n\n if (hours > 0)\n return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`\n return `${minutes}:${String(seconds).padStart(2, '0')}`\n}\n\n/**\n * Get a formatted description of the caffeinate duration.\n */\nexport function formatDuration(minutes: number): string {\n if (minutes <= 0 || minutes === -1)\n return 'Indefinitely'\n if (minutes < 60)\n return `${minutes} minutes`\n if (minutes === 60)\n return '1 hour'\n if (minutes % 60 === 0)\n return `${minutes / 60} hours`\n const h = Math.floor(minutes / 60)\n const m = minutes % 60\n return `${h}h ${m}m`\n}\n",
43
43
  "/**\n * Printing\n *\n * Print the current page (system print sheet) or save it as a PDF.\n * Uses `[WKWebView printOperationWithPrintInfo:]` on macOS — i.e.\n * the same code path Safari uses.\n *\n * Browser fallback uses `window.print()` for `print()`. There's no\n * portable web equivalent for \"silently save current page as PDF\" —\n * that path throws.\n */\nimport { hasBridge, requireBridge } from './_bridge'\n\nexport interface PrintToPDFResult {\n ok: boolean\n /** Where the PDF was written. */\n path?: string\n}\n\nexport interface PrintingAPI {\n /** Open the system print sheet for the current webview. */\n print: () => Promise<void>\n /** Render the current page to a PDF on disk. Path must be absolute. */\n printToPDF: (path: string) => Promise<PrintToPDFResult>\n}\n\nexport const printing: PrintingAPI = {\n async print() {\n if (hasBridge('printing')) {\n await window.craft!.printing.print()\n return\n }\n if (typeof window !== 'undefined' && typeof window.print === 'function') {\n window.print()\n }\n },\n\n async printToPDF(path: string): Promise<PrintToPDFResult> {\n if (!path) throw new Error('printToPDF: path is required')\n // Earlier we only accepted POSIX-style absolute paths (starts with\n // `/`), which broke on Windows where `C:\\Users\\...` is the norm.\n // Accept either: POSIX `/` prefix OR Windows drive-letter prefix\n // (`X:\\` or `X:/`). UNC paths (`\\\\server\\share`) also welcome.\n const isPosixAbs = path.startsWith('/')\n const isWinAbs = /^[a-zA-Z]:[\\\\/]/.test(path) || path.startsWith('\\\\\\\\')\n if (!isPosixAbs && !isWinAbs) {\n throw new Error('printToPDF: path must be absolute')\n }\n const r = await requireBridge('printing').printToPDF(path)\n return { ok: !!(r && r.ok), path: r?.path }\n },\n}\n",
44
44
  "/**\n * Display / Screen Info\n *\n * Multi-monitor info: every connected display's bounds, work area\n * (excluding menu bar + dock), and `backingScaleFactor` (1.0 / 2.0).\n *\n * Browser fallback returns a single Display synthesised from\n * `window.screen`, which is \"good enough\" for layout heuristics but\n * misses secondary monitors (browsers can't see those).\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface Display {\n /** Stable id within this app run (0 = primary). */\n id: number\n /** Full screen rect (origin in AppKit / display-space coords). */\n x: number\n y: number\n width: number\n height: number\n /** Work area excluding menu bar + dock. */\n workX: number\n workY: number\n workWidth: number\n workHeight: number\n /** 1.0 (Retina = 2.0). Multiply CSS px by this for device px. */\n scaleFactor: number\n}\n\nexport interface ScreenAPI {\n /** Every connected display. */\n getDisplays: () => Promise<Display[]>\n /** The primary display (the one with the menu bar). */\n getPrimary: () => Promise<Display | null>\n /**\n * Subscribe to display-arrangement changes — monitor hot-plug,\n * resolution change, dock relocation. The callback receives no\n * payload; re-fetch with `getDisplays()` to read the new state.\n */\n onChange: (cb: () => void) => () => void\n}\n\nexport const screen: ScreenAPI = {\n async getDisplays() {\n if (hasBridge('screen')) return await window.craft!.screen.getDisplays()\n return webDisplays()\n },\n async getPrimary() {\n if (hasBridge('screen')) {\n const r = await window.craft!.screen.getPrimary()\n return r && typeof r.width === 'number' ? r : null\n }\n return webDisplays()[0] ?? null\n },\n onChange(cb) {\n if (hasBridge('screen')) return onCraftEvent('craft:screen:change', () => cb())\n // Web fallback: window-level resize events. Not strictly equivalent\n // — they fire for window resizes too, not just monitor changes —\n // but it's the closest thing browsers expose.\n if (typeof window === 'undefined') return () => {}\n const h = () => cb()\n window.addEventListener('resize', h)\n return () => window.removeEventListener('resize', h)\n },\n}\n\nfunction webDisplays(): Display[] {\n if (typeof window === 'undefined' || !window.screen) return []\n const s = window.screen as any\n return [{\n id: 0,\n x: s.left ?? 0,\n y: s.top ?? 0,\n width: s.width || 0,\n height: s.height || 0,\n workX: s.availLeft ?? 0,\n workY: s.availTop ?? 0,\n workWidth: s.availWidth ?? s.width ?? 0,\n workHeight: s.availHeight ?? s.height ?? 0,\n scaleFactor: window.devicePixelRatio || 1,\n }]\n}\n",
package/dist/index.js.map CHANGED
@@ -25,7 +25,7 @@
25
25
  "/**\n * Drag-out\n *\n * Start a native OS drag from a DOM element so the user can drag a\n * file *out* of the app onto Finder, Slack, an email composer, etc.\n *\n * Browsers can't do this — `dataTransfer.setData('DownloadURL', ...)` is\n * Chrome-specific, deprecated, and gated behind weird user-gesture\n * rules. Inside a Craft window we install a real `NSDraggingItem`-based\n * drag, identical to dragging from Finder.\n *\n * Typical usage:\n *\n * const onMouseDown = (e: MouseEvent) => {\n * dragOut(['/Users/me/export.png'], { event: e })\n * }\n *\n * The mousedown event is preferred over a synthetic call because AppKit\n * uses the current mouse event to anchor the drag preview.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface DragOutOptions {\n /**\n * The mouse event that triggered the drag. Optional — when supplied,\n * the native side anchors the drag preview at the click point.\n */\n event?: MouseEvent\n /** Override anchor x. Used when `event` isn't available. */\n x?: number\n /** Override anchor y. Used when `event` isn't available. */\n y?: number\n}\n\n/**\n * Start a native drag with the given file path(s). Returns a promise\n * that resolves when the drag has *started* (not when it ends — there's\n * no reliable native callback for that without per-source delegate\n * scaffolding, and the OS handles the success path on the destination\n * side anyway).\n *\n * Outside a Craft window this rejects with a descriptive error so\n * callers can offer a fallback (e.g. trigger a download).\n */\nexport async function dragOut(paths: string | string[], options: DragOutOptions = {}): Promise<void> {\n if (!hasBridge('dragOut')) {\n throw new Error('dragOut requires a Craft native window')\n }\n const arr = Array.isArray(paths) ? paths : [paths]\n if (arr.length === 0) throw new Error('dragOut: at least one path required')\n await window.craft!.dragOut.start(arr, options)\n}\n\n/** True when native drag-out is available. */\nexport function isDragOutAvailable(): boolean {\n return hasBridge('dragOut')\n}\n",
26
26
  "/**\n * Deep Links (Custom URL Schemes)\n *\n * When the OS opens `myapp://path?foo=bar` the URL is delivered into\n * the running process. This module surfaces those URLs to your app.\n *\n * **Required setup on macOS:** declare the scheme in your `Info.plist`\n * under `CFBundleURLTypes`. Craft can't do that for you — bundling is\n * the app's responsibility — but with the entry present, the OS will\n * route URLs into Craft's AppleEvent handler, which fires `craft:deepLink`.\n *\n * **First-launch URLs:** if the app launched *because* the user clicked\n * a deep link, the URL may arrive before your subscriber attaches. Use\n * `getInitialUrl()` after subscribing to recover it.\n *\n * Browser fallback: when not in a Craft window, this module is a\n * graceful no-op (subscriptions never fire). For browser-side custom\n * URL schemes, register a service worker or use `navigator.registerProtocolHandler`.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface DeepLinkEvent {\n /** The full URL the OS delivered, exactly as received. */\n url: string\n}\n\nexport interface DeepLinks {\n /**\n * Subscribe to deep link arrivals. Returns an unsubscribe function.\n * Subscribers do NOT receive past URLs — pair with `getInitialUrl()`\n * to handle the case where the app launched in response to a URL.\n */\n onUrl: (cb: (e: DeepLinkEvent) => void) => () => void\n /**\n * Returns the URL that launched the app, if any. Idempotent — multiple\n * reads return the same value. Use `consumeInitialUrl()` if you only\n * want to handle the launch URL once.\n */\n getInitialUrl: () => string | null\n /**\n * Like `getInitialUrl()` but clears the stored URL after reading.\n * Useful for \"process pending deep link on boot, ignore on re-render\"\n * flows in framework code that mounts/remounts repeatedly.\n */\n consumeInitialUrl: () => string | null\n /** True when the deep-link bridge is available. */\n isAvailable: () => boolean\n}\n\nexport const deepLinks: DeepLinks = {\n onUrl(cb): () => void {\n if (!hasBridge('deepLink')) return () => {}\n return onCraftEvent<DeepLinkEvent>('craft:deepLink', cb)\n },\n getInitialUrl(): string | null {\n if (typeof window === 'undefined') return null\n if (hasBridge('deepLink')) {\n try { return window.craft!.deepLink.getInitialUrl() } catch { return null }\n }\n return window.__craftPendingDeepLink || null\n },\n consumeInitialUrl(): string | null {\n const url = this.getInitialUrl()\n // Native side stores its own copy; we can only clear the JS-side\n // mirror. Subsequent `getInitialUrl()` calls into the bridge will\n // still return the URL until a new one arrives, but\n // `consumeInitialUrl()` returns null on second read, which matches\n // the consume-once contract callers expect.\n if (typeof window !== 'undefined') window.__craftPendingDeepLink = undefined\n return url\n },\n isAvailable(): boolean {\n return hasBridge('deepLink')\n },\n}\n",
27
27
  "/**\n * Battery & Power State\n *\n * Read battery level, charging status, thermal pressure, and tell the\n * OS to keep the system awake while a long task runs.\n *\n * **Different from `power.ts`** — that module wraps the macOS\n * `caffeinate` *subprocess* (works without a Craft window). This\n * module talks to the native `craft.power` bridge for richer metrics\n * (battery %, thermal state) and uses IOKit's\n * `IOPMAssertionCreateWithName` instead of spawning a child process.\n *\n * Browser fallback: most fields use the deprecated `BatteryManager`\n * API where it's still available; `preventSleep`/`allowSleep` use the\n * Wake Lock API (`navigator.wakeLock.request('screen')`).\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type ThermalState = 'nominal' | 'fair' | 'serious' | 'critical' | 'unknown'\n\nexport interface BatteryAPI {\n /** True if currently charging (plugged in AND filling). */\n isCharging: () => Promise<boolean>\n /** True if connected to AC power, regardless of fill state. */\n isPluggedIn: () => Promise<boolean>\n /** True if Low Power Mode is on. */\n isLowPowerMode: () => Promise<boolean>\n /**\n * Battery fill, 0..1. Returns null when the system has no battery\n * (typical desktops) so callers can distinguish \"no battery\" from\n * \"fully charged\".\n */\n level: () => Promise<number | null>\n /** Minutes until empty (discharging) or full (charging). null if unknown. */\n timeRemaining: () => Promise<number | null>\n /** OS thermal state. */\n thermalState: () => Promise<ThermalState>\n /** System uptime in seconds. */\n uptimeSeconds: () => Promise<number>\n /**\n * Tell the OS to keep the display/system awake.\n * `reason` is shown in tools like Activity Monitor.\n */\n preventSleep: (reason?: string) => Promise<void>\n /** Release a previous `preventSleep` call. Idempotent. */\n allowSleep: () => Promise<void>\n /** Subscribe to OS sleep events. */\n onSleep: (cb: () => void) => () => void\n /** Subscribe to OS wake events. */\n onWake: (cb: () => void) => () => void\n}\n\nexport const battery: BatteryAPI = {\n async isCharging() {\n if (hasBridge('power')) return await window.craft!.power.isCharging()\n const b = await getWebBatteryManager()\n return b ? !!b.charging : false\n },\n\n async isPluggedIn() {\n if (hasBridge('power')) return await window.craft!.power.isPluggedIn()\n const b = await getWebBatteryManager()\n // BatteryManager has no \"pluggedIn\" — proxy via charging-or-fully-charged.\n return b ? !!b.charging || b.level >= 0.999 : false\n },\n\n async isLowPowerMode() {\n if (hasBridge('power')) return await window.craft!.power.isLowPowerMode()\n return false\n },\n\n async level() {\n if (hasBridge('power')) {\n const v = await window.craft!.power.batteryLevel()\n return typeof v === 'number' ? v : null\n }\n const b = await getWebBatteryManager()\n return b ? b.level : null\n },\n\n async timeRemaining() {\n if (hasBridge('power')) {\n const r = await window.craft!.power.timeRemaining()\n return typeof r === 'number' ? r : null\n }\n const b = await getWebBatteryManager()\n if (!b) return null\n // BatteryManager reports seconds; we return minutes for parity with\n // the native side. Infinity means \"unknown / not on battery.\"\n const sec = b.charging ? b.chargingTime : b.dischargingTime\n return Number.isFinite(sec) ? Math.round(sec / 60) : null\n },\n\n async thermalState() {\n if (hasBridge('power')) {\n const s = await window.craft!.power.thermalState()\n return (s as ThermalState) || 'unknown'\n }\n return 'unknown'\n },\n\n async uptimeSeconds() {\n if (hasBridge('power')) return await window.craft!.power.uptimeSeconds()\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return Math.round(performance.now() / 1000)\n }\n return 0\n },\n\n async preventSleep(reason = 'app is busy') {\n if (hasBridge('power')) {\n await window.craft!.power.preventSleep(reason)\n return\n }\n // Web fallback: Screen Wake Lock API. Earlier this overwrote the\n // sentinel on every call and silently leaked the previous lock.\n // Now release any in-flight lock first, then acquire fresh — same\n // observable behaviour from the caller's perspective, no leak.\n if (typeof navigator === 'undefined' || !(navigator as any).wakeLock) return\n const w = window as any\n if (w.__craftWebWakeLock?.release) {\n try { await w.__craftWebWakeLock.release() } catch { /* ignore */ }\n w.__craftWebWakeLock = null\n }\n try {\n const sentinel = await (navigator as any).wakeLock.request('screen')\n w.__craftWebWakeLock = sentinel\n }\n catch { /* user denied or unsupported */ }\n },\n\n async allowSleep() {\n if (hasBridge('power')) {\n await window.craft!.power.allowSleep()\n return\n }\n const s = (window as any).__craftWebWakeLock\n if (s && typeof s.release === 'function') {\n try { await s.release() } catch { /* ignore */ }\n ;(window as any).__craftWebWakeLock = null\n }\n },\n\n onSleep(cb) {\n return onCraftEvent<void>('craft:powerSleep', cb)\n },\n onWake(cb) {\n return onCraftEvent<void>('craft:powerWake', cb)\n },\n}\n\nasync function getWebBatteryManager(): Promise<any | null> {\n if (typeof navigator === 'undefined') return null\n const nav = navigator as any\n if (typeof nav.getBattery !== 'function') return null\n try { return await nav.getBattery() } catch { return null }\n}\n",
28
- "/**\n * Network / Reachability\n *\n * Connection type, WiFi info, IP/MAC addresses, VPN status, proxy\n * settings. When running in a Craft native window this dispatches to\n * the `craft.network` bridge (SystemConfiguration on macOS / NLM on\n * Windows / NetworkManager on Linux). Browser fallback uses the\n * `navigator.connection` API where available.\n *\n * Some fields (MAC address, VPN, proxy) are unavailable in browsers\n * for security reasons — they return empty/false there rather than\n * throwing, so feature-detection on the result is the cleanest pattern.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type ConnectionType = \n| 'wifi'\n| 'ethernet'\n| 'cellular'\n| 'bluetooth'\n| 'vpn'\n| 'none'\n| 'unknown'\n\nexport interface NetworkInterface {\n name: string\n /** IPv4/IPv6 address. */\n address: string\n /** True if the interface is up and has carrier. */\n isUp: boolean\n /** True for loopback (127.0.0.1 / ::1). */\n isLoopback: boolean\n}\n\nexport interface ProxySettings {\n http?: string\n https?: string\n ftp?: string\n socks?: string\n /** Hosts that should bypass the proxy. */\n exceptions?: string[]\n}\n\nexport interface NetworkAPI {\n /** Coarse-grained type of the active connection. */\n connectionType: () => Promise<ConnectionType>\n /** SSID of the joined WiFi network, or undefined if not on WiFi. */\n wifiSSID: () => Promise<string | undefined>\n /** Signal strength in dBm (negative — closer to 0 = stronger). */\n wifiSignalStrength: () => Promise<number | undefined>\n /** Primary IP address. */\n ipAddress: () => Promise<string>\n /** Hardware address of the active interface. May be empty in browsers. */\n macAddress: () => Promise<string>\n /** Every active network interface. */\n interfaces: () => Promise<NetworkInterface[]>\n /** True if a VPN tunnel is up. False on web (always). */\n isVPNConnected: () => Promise<boolean>\n /** System proxy settings. Empty object on web. */\n proxySettings: () => Promise<ProxySettings>\n /** Open the system Network preference pane / settings page. */\n openPreferences: () => Promise<void>\n /** Subscribe to reachability changes. */\n onChange: (cb: (info: { type: ConnectionType, online: boolean }) => void) => () => void\n}\n\nexport const network: NetworkAPI = {\n async connectionType() {\n if (hasBridge('network')) return await window.craft!.network.connectionType()\n return webConnectionType()\n },\n async wifiSSID() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSSID()\n return v || undefined\n }\n return undefined\n },\n async wifiSignalStrength() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSignalStrength()\n return typeof v === 'number' ? v : undefined\n }\n return undefined\n },\n async ipAddress() {\n if (hasBridge('network')) return await window.craft!.network.ipAddress()\n return ''\n },\n async macAddress() {\n if (hasBridge('network')) return await window.craft!.network.macAddress()\n return ''\n },\n async interfaces() {\n if (hasBridge('network')) return await window.craft!.network.interfaces()\n return []\n },\n async isVPNConnected() {\n if (hasBridge('network')) return await window.craft!.network.isVPNConnected()\n return false\n },\n async proxySettings() {\n if (hasBridge('network')) {\n const r = await window.craft!.network.proxySettings()\n return r || {}\n }\n return {}\n },\n async openPreferences() {\n if (hasBridge('network')) await window.craft!.network.openPreferences()\n },\n onChange(cb): () => void {\n if (hasBridge('network')) {\n return onCraftEvent<{ type: ConnectionType, online: boolean }>('craft:networkChange', cb)\n }\n if (typeof window === 'undefined') return () => {}\n const onlineH = () => cb({ type: webConnectionType(), online: true })\n const offlineH = () => cb({ type: 'none', online: false })\n window.addEventListener('online', onlineH)\n window.addEventListener('offline', offlineH)\n return () => {\n window.removeEventListener('online', onlineH)\n window.removeEventListener('offline', offlineH)\n }\n },\n}\n\nfunction webConnectionType(): ConnectionType {\n if (typeof navigator === 'undefined') return 'unknown'\n if (navigator.onLine === false) return 'none'\n const conn = (navigator as any).connection\n if (!conn) return 'unknown'\n // navigator.connection.type: 'wifi'|'cellular'|'ethernet'|'bluetooth'|'wimax'|'none'|'other'|'unknown'\n const t = String(conn.type || conn.effectiveType || 'unknown').toLowerCase()\n if (t === 'wifi' || t === 'cellular' || t === 'ethernet' || t === 'bluetooth' || t === 'none') return t\n return 'unknown'\n}\n",
28
+ "/**\n * Network / Reachability\n *\n * Connection type, WiFi info, IP/MAC addresses, VPN status, proxy\n * settings. When running in a Craft native window this dispatches to\n * the `craft.network` bridge (SystemConfiguration on macOS / NLM on\n * Windows / NetworkManager on Linux). Browser fallback uses the\n * `navigator.connection` API where available.\n *\n * Some fields (MAC address, VPN, proxy) are unavailable in browsers\n * for security reasons — they return empty/false there rather than\n * throwing, so feature-detection on the result is the cleanest pattern.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type ConnectionType =\n| 'wifi'\n| 'ethernet'\n| 'cellular'\n| 'bluetooth'\n| 'vpn'\n| 'none'\n| 'unknown'\n\nexport interface NetworkInterface {\n name: string\n /** IPv4/IPv6 address. */\n address: string\n /** True if the interface is up and has carrier. */\n isUp: boolean\n /** True for loopback (127.0.0.1 / ::1). */\n isLoopback: boolean\n}\n\nexport interface ProxySettings {\n http?: string\n https?: string\n ftp?: string\n socks?: string\n /** Hosts that should bypass the proxy. */\n exceptions?: string[]\n}\n\nexport interface NetworkAPI {\n /** Coarse-grained type of the active connection. */\n connectionType: () => Promise<ConnectionType>\n /** SSID of the joined WiFi network, or undefined if not on WiFi. */\n wifiSSID: () => Promise<string | undefined>\n /** Signal strength in dBm (negative — closer to 0 = stronger). */\n wifiSignalStrength: () => Promise<number | undefined>\n /** Primary IP address. */\n ipAddress: () => Promise<string>\n /** Hardware address of the active interface. May be empty in browsers. */\n macAddress: () => Promise<string>\n /** Every active network interface. */\n interfaces: () => Promise<NetworkInterface[]>\n /** True if a VPN tunnel is up. False on web (always). */\n isVPNConnected: () => Promise<boolean>\n /** System proxy settings. Empty object on web. */\n proxySettings: () => Promise<ProxySettings>\n /** Open the system Network preference pane / settings page. */\n openPreferences: () => Promise<void>\n /** Subscribe to reachability changes. */\n onChange: (cb: (info: { type: ConnectionType, online: boolean }) => void) => () => void\n}\n\nexport const network: NetworkAPI = {\n async connectionType() {\n if (hasBridge('network')) return await window.craft!.network.connectionType()\n return webConnectionType()\n },\n async wifiSSID() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSSID()\n return v || undefined\n }\n return undefined\n },\n async wifiSignalStrength() {\n if (hasBridge('network')) {\n const v = await window.craft!.network.wifiSignalStrength()\n return typeof v === 'number' ? v : undefined\n }\n return undefined\n },\n async ipAddress() {\n if (hasBridge('network')) return await window.craft!.network.ipAddress()\n return ''\n },\n async macAddress() {\n if (hasBridge('network')) return await window.craft!.network.macAddress()\n return ''\n },\n async interfaces() {\n if (hasBridge('network')) return await window.craft!.network.interfaces()\n return []\n },\n async isVPNConnected() {\n if (hasBridge('network')) return await window.craft!.network.isVPNConnected()\n return false\n },\n async proxySettings() {\n if (hasBridge('network')) {\n const r = await window.craft!.network.proxySettings()\n return r || {}\n }\n return {}\n },\n async openPreferences() {\n if (hasBridge('network')) await window.craft!.network.openPreferences()\n },\n onChange(cb): () => void {\n if (hasBridge('network')) {\n return onCraftEvent<{ type: ConnectionType, online: boolean }>('craft:networkChange', cb)\n }\n if (typeof window === 'undefined') return () => {}\n const onlineH = () => cb({ type: webConnectionType(), online: true })\n const offlineH = () => cb({ type: 'none', online: false })\n window.addEventListener('online', onlineH)\n window.addEventListener('offline', offlineH)\n return () => {\n window.removeEventListener('online', onlineH)\n window.removeEventListener('offline', offlineH)\n }\n },\n}\n\nfunction webConnectionType(): ConnectionType {\n if (typeof navigator === 'undefined') return 'unknown'\n if (navigator.onLine === false) return 'none'\n const conn = (navigator as any).connection\n if (!conn) return 'unknown'\n // navigator.connection.type: 'wifi'|'cellular'|'ethernet'|'bluetooth'|'wimax'|'none'|'other'|'unknown'\n const t = String(conn.type || conn.effectiveType || 'unknown').toLowerCase()\n if (t === 'wifi' || t === 'cellular' || t === 'ethernet' || t === 'bluetooth' || t === 'none') return t\n return 'unknown'\n}\n",
29
29
  "/**\n * Auto-Updater\n *\n * Configure and trigger app updates. On macOS this routes to Sparkle\n * (`SUUpdater`); on Windows to WinSparkle; on Linux to a custom feed\n * checker. The shape of the API is provider-neutral — a feed URL plus\n * \"check now\" / \"check in background.\"\n *\n * Apps still need to bundle the updater framework and ship signed\n * appcasts; Craft only exposes the runtime knobs and event surface.\n *\n * Browser fallback: this module is an inert no-op (calls resolve, but\n * nothing happens). Outside a native shell there's no update flow.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface UpdateInfo {\n version: string\n releaseDate?: string\n releaseNotes?: string\n /** Direct download URL for the new build. */\n downloadUrl?: string\n}\n\nexport interface VerifyOptions {\n /** Raw bytes of the update bundle (zip / dmg / msi / appimage). */\n payload: Uint8Array | ArrayBuffer\n /** Base64-encoded Ed25519 signature over `payload`. */\n signatureB64: string\n /** Base64-encoded raw Ed25519 public key (32 bytes). */\n publicKeyB64: string\n}\n\nexport interface VerifyDownloadOptions {\n /** Where to fetch the bundle from. */\n url: string\n /** Base64-encoded Ed25519 signature over the bundle bytes. */\n signatureB64: string\n /** Base64-encoded raw Ed25519 public key (32 bytes). */\n publicKeyB64: string\n /** Optional fetch init (timeout / auth headers / etc.). */\n fetchInit?: RequestInit\n}\n\nexport interface VerifyDownloadResult {\n ok: boolean\n /** The downloaded bytes — only populated when `ok === true`. */\n payload?: Uint8Array\n /** When `ok === false`, why. */\n reason?: 'fetch-failed' | 'http-error' | 'bad-signature' | 'bad-key'\n /** HTTP status when applicable. */\n status?: number\n}\n\nexport interface Updater {\n /** Show the updater UI and check for updates. */\n checkForUpdates: () => Promise<void>\n /** Silent check — no UI unless an update is found. */\n checkInBackground: () => Promise<void>\n /** Toggle scheduled background checks. */\n setAutomaticChecks: (on: boolean) => Promise<void>\n /** Background check interval in seconds. Default is provider-defined. */\n setCheckInterval: (seconds: number) => Promise<void>\n /** Set the appcast URL. Persists across launches. */\n setFeedURL: (url: string) => Promise<void>\n /** ISO date of the most recent check, or null. */\n getLastUpdateCheckDate: () => Promise<string | null>\n /** Latest known update info if an update has been found, else null. */\n getUpdateInfo: () => Promise<UpdateInfo | null>\n /** Subscribe to \"update available\" events. */\n onAvailable: (cb: (info: UpdateInfo) => void) => () => void\n /** Subscribe to \"update downloaded and ready to install\" events. */\n onDownloaded: (cb: (info: UpdateInfo) => void) => () => void\n /**\n * Verify an in-memory bundle against an Ed25519 signature. Sparkle-style:\n * publisher signs the file with their Ed25519 private key, the app pins\n * the matching public key, and rejects bundles whose signature doesn't\n * verify before staging the install.\n */\n verifySignature: (options: VerifyOptions) => Promise<boolean>\n /**\n * Fetch a bundle and verify its signature in one shot. On success the\n * downloaded bytes come back so the caller can hand them to the native\n * installer; on failure the `reason` says why so the UI can surface it.\n */\n verifyDownload: (options: VerifyDownloadOptions) => Promise<VerifyDownloadResult>\n}\n\nexport const updater: Updater = {\n async checkForUpdates() {\n if (!hasBridge('updater')) return\n await window.craft!.updater.checkForUpdates()\n },\n async checkInBackground() {\n if (!hasBridge('updater')) return\n await window.craft!.updater.checkInBackground()\n },\n async setAutomaticChecks(on: boolean) {\n if (!hasBridge('updater')) return\n await window.craft!.updater.setAutomaticChecks(on)\n },\n async setCheckInterval(seconds: number) {\n if (!hasBridge('updater')) return\n // Sparkle / WinSparkle interpret a 0 or negative interval as\n // \"disable scheduled checks\" via the API of their respective\n // platforms — but several callers report unstable behaviour when\n // setting a tiny positive interval (e.g. 1s = check storm). Clamp\n // to a sensible range: ≤0 disables, ≥60s otherwise.\n if (!Number.isFinite(seconds)) {\n throw new Error('setCheckInterval: must be a finite number')\n }\n const safe = seconds <= 0 ? 0 : Math.max(60, Math.round(seconds))\n await window.craft!.updater.setCheckInterval(safe)\n },\n async setFeedURL(url: string) {\n if (!hasBridge('updater')) return\n await window.craft!.updater.setFeedURL(url)\n },\n async getLastUpdateCheckDate() {\n if (!hasBridge('updater')) return null\n const v = await window.craft!.updater.getLastUpdateCheckDate()\n return v || null\n },\n async getUpdateInfo() {\n if (!hasBridge('updater')) return null\n const v = await window.craft!.updater.getUpdateInfo()\n // Earlier `!v.version` rejected `'0.0.0'` (a valid downgrade\n // marker some apps use during testing). Distinguish \"no info\" from\n // \"info with version\" by checking presence of the field.\n if (!v || typeof v.version !== 'string' || v.version.length === 0) return null\n return v as UpdateInfo\n },\n onAvailable(cb) {\n return onCraftEvent<UpdateInfo>('craft:updateAvailable', cb)\n },\n onDownloaded(cb) {\n return onCraftEvent<UpdateInfo>('craft:updateDownloaded', cb)\n },\n\n async verifySignature({ payload, signatureB64, publicKeyB64 }) {\n const data = toArrayBufferBytes(payload)\n let publicKey: CryptoKey\n try {\n publicKey = await crypto.subtle.importKey(\n 'raw',\n base64ToBytes(publicKeyB64),\n { name: 'Ed25519' },\n false,\n ['verify'],\n )\n }\n catch {\n return false\n }\n try {\n return await crypto.subtle.verify('Ed25519', publicKey, base64ToBytes(signatureB64), data)\n }\n catch {\n return false\n }\n },\n\n async verifyDownload({ url, signatureB64, publicKeyB64, fetchInit }) {\n let response: Response\n try {\n response = await fetch(url, fetchInit)\n }\n catch {\n return { ok: false, reason: 'fetch-failed' }\n }\n if (!response.ok) {\n return { ok: false, reason: 'http-error', status: response.status }\n }\n const buffer = new Uint8Array(await response.arrayBuffer())\n\n let publicKey: CryptoKey\n try {\n publicKey = await crypto.subtle.importKey(\n 'raw',\n base64ToBytes(publicKeyB64),\n { name: 'Ed25519' },\n false,\n ['verify'],\n )\n }\n catch {\n return { ok: false, reason: 'bad-key' }\n }\n\n let valid = false\n try {\n valid = await crypto.subtle.verify('Ed25519', publicKey, base64ToBytes(signatureB64), toArrayBufferBytes(buffer))\n }\n catch {\n valid = false\n }\n if (!valid) return { ok: false, reason: 'bad-signature' }\n return { ok: true, payload: buffer }\n },\n}\n\n/**\n * Coerce arbitrary `BufferSource`-ish input into a `Uint8Array` whose\n * underlying storage is a plain `ArrayBuffer`. The Web Crypto types\n * insist on `ArrayBufferView<ArrayBuffer>`, which excludes the looser\n * `ArrayBufferLike` (covers `SharedArrayBuffer`) you get back from\n * `Response.arrayBuffer()` under TS strict mode.\n */\nfunction toArrayBufferBytes(input: Uint8Array | ArrayBuffer): Uint8Array<ArrayBuffer> {\n if (input instanceof ArrayBuffer) return new Uint8Array(input)\n // Copy into a fresh ArrayBuffer to satisfy the strict crypto types\n // even when the source view points at a SharedArrayBuffer.\n const out = new ArrayBuffer(input.byteLength)\n const view = new Uint8Array(out)\n view.set(input)\n return view\n}\n\nfunction base64ToBytes(b64: string): Uint8Array<ArrayBuffer> {\n // atob exists in webview + Bun; if neither (older Node test runner), fall\n // back to a hand-rolled decoder so this module loads everywhere.\n if (typeof atob === 'function') {\n const bin = atob(b64)\n const buffer = new ArrayBuffer(bin.length)\n const out = new Uint8Array(buffer)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return out\n }\n // eslint-disable-next-line node/prefer-global/buffer\n const node = Buffer.from(b64, 'base64')\n const buf = new ArrayBuffer(node.length)\n new Uint8Array(buf).set(node)\n return new Uint8Array(buf)\n}\n",
30
30
  "/**\n * Window Lifecycle Events\n *\n * Subscribe to focus, blur, resize, move, minimize, restore, and close\n * events on the main Craft window. Backed by an `NSWindowDelegate` on\n * the macOS side; in browser builds these fall back to the equivalent\n * `window.addEventListener('focus' | 'blur' | 'resize' | 'beforeunload')`.\n *\n * **Note on close:** the native `onClose` event fires *after* AppKit\n * has committed to closing — you can't `event.preventDefault()` from\n * here. For a real \"are you sure you want to close?\" gate, attach\n * `beforeunload` in JS, which fires before AppKit's close path runs.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface WindowSize {\n /**\n * Stable window identifier — opaque string. In multi-window apps, use\n * this to correlate events back to the right window. Single-window\n * apps can ignore it.\n */\n id?: string\n width: number\n height: number\n}\n\nexport interface WindowPosition {\n id?: string\n x: number\n y: number\n}\n\nexport interface WindowEvents {\n /** Window became key (received focus). */\n onFocus: (cb: () => void) => () => void\n /** Window lost focus / resigned key state. */\n onBlur: (cb: () => void) => () => void\n /** Window was resized. Detail contains the new size. */\n onResize: (cb: (size: WindowSize) => void) => () => void\n /** Window was moved. Detail contains the new origin. */\n onMove: (cb: (pos: WindowPosition) => void) => () => void\n /** Window is closing (fires after AppKit commits — not interceptable). */\n onClose: (cb: () => void) => () => void\n /** Window was minimized to the dock. */\n onMinimize: (cb: () => void) => () => void\n /** Window was restored from minimize. */\n onRestore: (cb: () => void) => () => void\n}\n\nexport const windowEvents: WindowEvents = {\n onFocus(cb) {\n if (hasBridge('window')) return onCraftEvent('craft:window:focus', () => cb())\n return webEvent('focus', cb)\n },\n onBlur(cb) {\n if (hasBridge('window')) return onCraftEvent('craft:window:blur', () => cb())\n return webEvent('blur', cb)\n },\n onResize(cb) {\n if (hasBridge('window')) return onCraftEvent<WindowSize>('craft:window:resize', cb)\n if (typeof window === 'undefined') return () => {}\n const h = () => cb({ width: window.innerWidth, height: window.innerHeight })\n window.addEventListener('resize', h)\n return () => window.removeEventListener('resize', h)\n },\n onMove(cb) {\n if (hasBridge('window')) return onCraftEvent<WindowPosition>('craft:window:move', cb)\n // Browsers don't expose window-position changes, so this is no-op\n // outside Craft. Returning the same shape keeps callers branchless.\n return () => {}\n },\n onClose(cb) {\n if (hasBridge('window')) return onCraftEvent('craft:window:close', () => cb())\n return webEvent('beforeunload', cb)\n },\n onMinimize(cb) {\n if (hasBridge('window')) return onCraftEvent('craft:window:minimize', () => cb())\n // Closest browser equivalent is visibilitychange → 'hidden'.\n if (typeof document === 'undefined') return () => {}\n const h = () => { if (document.visibilityState === 'hidden') cb() }\n document.addEventListener('visibilitychange', h)\n return () => document.removeEventListener('visibilitychange', h)\n },\n onRestore(cb) {\n if (hasBridge('window')) return onCraftEvent('craft:window:restore', () => cb())\n if (typeof document === 'undefined') return () => {}\n const h = () => { if (document.visibilityState === 'visible') cb() }\n document.addEventListener('visibilitychange', h)\n return () => document.removeEventListener('visibilitychange', h)\n },\n}\n\nfunction webEvent(name: string, cb: () => void): () => void {\n if (typeof window === 'undefined') return () => {}\n const h = () => cb()\n window.addEventListener(name, h)\n return () => window.removeEventListener(name, h)\n}\n",
31
31
  "/**\n * App Metadata + Process Controls\n *\n * Companion to `window.ts`. While that module is about the visible\n * window, this is about the *application* — bundle metadata, dock\n * badge, dock-icon bounce.\n *\n * Outside a Craft window, the metadata getters return best-effort\n * defaults (empty / \"0.0.0\") so call sites can render an About panel\n * without branching on environment.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface AppInfo {\n /** Bundle name (e.g. \"Photo Booth\"). */\n name: string\n /** CFBundleShortVersionString — the user-facing version. */\n version: string\n /** Bundle identifier (e.g. \"com.example.app\"). */\n bundleId?: string\n /** Absolute path to the .app bundle. */\n bundlePath?: string\n /** Path to the executable inside the bundle. */\n executablePath?: string\n}\n\nexport interface AppNotifyOptions {\n title: string\n body?: string\n /** macOS UNNotificationSound name, or 'default'. */\n sound?: string\n}\n\nexport interface AppAPI {\n /** Hide the dock icon (turn the app into a menubar-only / accessory app). */\n hideDockIcon: () => Promise<void>\n /** Restore the dock icon. */\n showDockIcon: () => Promise<void>\n /** Quit the application. */\n quit: () => Promise<void>\n /** Read bundle metadata. Resolves to defaults outside Craft. */\n getInfo: () => Promise<AppInfo>\n /** Post a system notification (alias for `notifications.show` with smaller surface). */\n notify: (options: AppNotifyOptions) => Promise<void>\n /** Set the dock-icon badge. Pass 0 to clear. */\n setBadge: (count: number) => Promise<void>\n /**\n * Bounce the dock icon to draw attention.\n * `'critical'` keeps bouncing until the user activates the app;\n * `'informational'` bounces once.\n */\n bounce: (type?: 'critical' | 'informational') => Promise<void>\n}\n\nconst DEFAULT_INFO: AppInfo = { name: '', version: '0.0.0' }\n\nexport const app: AppAPI = {\n async hideDockIcon() { if (hasBridge('app')) await window.craft!.app.hideDockIcon() },\n async showDockIcon() { if (hasBridge('app')) await window.craft!.app.showDockIcon() },\n async quit() { if (hasBridge('app')) await window.craft!.app.quit() },\n async getInfo() {\n if (!hasBridge('app')) return DEFAULT_INFO\n const r = await window.craft!.app.getInfo()\n return { ...DEFAULT_INFO, ...(r || {}) }\n },\n async notify(options) {\n if (!options.title) throw new Error('notify: title is required')\n if (hasBridge('app')) await window.craft!.app.notify(options)\n },\n async setBadge(count: number) {\n if (hasBridge('app')) await window.craft!.app.setBadge(count)\n },\n async bounce(type = 'informational') {\n if (hasBridge('app')) await window.craft!.app.bounce(type)\n },\n}\n",
@@ -33,17 +33,17 @@
33
33
  "/**\n * System / Host Info\n *\n * Read the OS-level facts an app needs for \"render correctly on this\n * machine\" — accent colour, locale, timezone, accessibility flags\n * (reduce motion / contrast / transparency), system version, hostname.\n *\n * In a browser, falls back to whatever standard web APIs surface\n * (mostly `Intl.DateTimeFormat().resolvedOptions()` + `matchMedia` for\n * the accessibility flags).\n */\nimport { hasBridge } from './_bridge'\n\nexport interface SystemInfo {\n /** macOS accent colour as a hex string (\"#0a84ff\"). Empty in browser. */\n accentColor: () => Promise<string>\n /** Selection / text-selection highlight colour. Empty in browser. */\n highlightColor: () => Promise<string>\n /** Primary user language code (e.g. \"en\"). */\n language: () => Promise<string>\n /** BCP-47 locale (e.g. \"en-US\"). */\n locale: () => Promise<string>\n /** IANA timezone name (e.g. \"America/Los_Angeles\"). */\n timezone: () => Promise<string>\n /** True if the user prefers 24-hour time. */\n is24HourTime: () => Promise<boolean>\n /** True if the user has Reduce Motion enabled. */\n reduceMotion: () => Promise<boolean>\n /** True if the user has Reduce Transparency enabled. */\n reduceTransparency: () => Promise<boolean>\n /** True if the user has Increase Contrast enabled. */\n increaseContrast: () => Promise<boolean>\n /** OS version string (e.g. \"14.4.1\"). */\n systemVersion: () => Promise<string>\n /** Machine hostname. Empty in browser. */\n hostname: () => Promise<string>\n /** Login username. Empty in browser. */\n username: () => Promise<string>\n /** Open System Settings / Preferences. */\n openPreferences: () => Promise<void>\n}\n\nexport const system: SystemInfo = {\n accentColor: () => bridgeOr('system', 'accentColor', () => ''),\n highlightColor: () => bridgeOr('system', 'highlightColor', () => ''),\n language: () => bridgeOr('system', 'language', () => {\n if (typeof navigator === 'undefined') return ''\n return navigator.language?.split('-')[0] || ''\n }),\n locale: () => bridgeOr('system', 'locale', () => {\n if (typeof navigator === 'undefined') return ''\n return navigator.language || ''\n }),\n timezone: () => bridgeOr('system', 'timezone', () => {\n try { return Intl.DateTimeFormat().resolvedOptions().timeZone || '' } catch { return '' }\n }),\n is24HourTime: () => bridgeOr('system', 'is24HourTime', () => {\n try {\n const opts = new Intl.DateTimeFormat([], { hour: 'numeric' }).resolvedOptions() as any\n return opts.hourCycle === 'h23' || opts.hourCycle === 'h24'\n }\n catch { return false }\n }),\n reduceMotion: () => bridgeOr('system', 'reduceMotion', () => mediaMatches('(prefers-reduced-motion: reduce)')),\n reduceTransparency: () => bridgeOr('system', 'reduceTransparency', () => mediaMatches('(prefers-reduced-transparency: reduce)')),\n increaseContrast: () => bridgeOr('system', 'increaseContrast', () => mediaMatches('(prefers-contrast: more)')),\n systemVersion: () => bridgeOr('system', 'systemVersion', () => ''),\n hostname: () => bridgeOr('system', 'hostname', () => ''),\n username: () => bridgeOr('system', 'username', () => ''),\n async openPreferences() {\n if (hasBridge('system')) await window.craft!.system.openPreferences()\n },\n}\n\nasync function bridgeOr<T>(ns: string, method: string, fallback: () => T | Promise<T>): Promise<T> {\n if (hasBridge(ns)) return (await window.craft![ns][method]()) as T\n return await fallback()\n}\n\nfunction mediaMatches(query: string): boolean {\n if (typeof window === 'undefined' || !window.matchMedia) return false\n return window.matchMedia(query).matches\n}\n",
34
34
  "/**\n * Display / Screen Info\n *\n * Multi-monitor info: every connected display's bounds, work area\n * (excluding menu bar + dock), and `backingScaleFactor` (1.0 / 2.0).\n *\n * Browser fallback returns a single Display synthesised from\n * `window.screen`, which is \"good enough\" for layout heuristics but\n * misses secondary monitors (browsers can't see those).\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface Display {\n /** Stable id within this app run (0 = primary). */\n id: number\n /** Full screen rect (origin in AppKit / display-space coords). */\n x: number\n y: number\n width: number\n height: number\n /** Work area excluding menu bar + dock. */\n workX: number\n workY: number\n workWidth: number\n workHeight: number\n /** 1.0 (Retina = 2.0). Multiply CSS px by this for device px. */\n scaleFactor: number\n}\n\nexport interface ScreenAPI {\n /** Every connected display. */\n getDisplays: () => Promise<Display[]>\n /** The primary display (the one with the menu bar). */\n getPrimary: () => Promise<Display | null>\n /**\n * Subscribe to display-arrangement changes — monitor hot-plug,\n * resolution change, dock relocation. The callback receives no\n * payload; re-fetch with `getDisplays()` to read the new state.\n */\n onChange: (cb: () => void) => () => void\n}\n\nexport const screen: ScreenAPI = {\n async getDisplays() {\n if (hasBridge('screen')) return await window.craft!.screen.getDisplays()\n return webDisplays()\n },\n async getPrimary() {\n if (hasBridge('screen')) {\n const r = await window.craft!.screen.getPrimary()\n return r && typeof r.width === 'number' ? r : null\n }\n return webDisplays()[0] ?? null\n },\n onChange(cb) {\n if (hasBridge('screen')) return onCraftEvent('craft:screen:change', () => cb())\n // Web fallback: window-level resize events. Not strictly equivalent\n // — they fire for window resizes too, not just monitor changes —\n // but it's the closest thing browsers expose.\n if (typeof window === 'undefined') return () => {}\n const h = () => cb()\n window.addEventListener('resize', h)\n return () => window.removeEventListener('resize', h)\n },\n}\n\nfunction webDisplays(): Display[] {\n if (typeof window === 'undefined' || !window.screen) return []\n const s = window.screen as any\n return [{\n id: 0,\n x: s.left ?? 0,\n y: s.top ?? 0,\n width: s.width || 0,\n height: s.height || 0,\n workX: s.availLeft ?? 0,\n workY: s.availTop ?? 0,\n workWidth: s.availWidth ?? s.width ?? 0,\n workHeight: s.availHeight ?? s.height ?? 0,\n scaleFactor: window.devicePixelRatio || 1,\n }]\n}\n",
35
35
  "/**\n * Keychain — Secure Secret Storage\n *\n * Wraps platform-specific credential stores: macOS Keychain Services,\n * iOS Keychain, Windows Credential Manager, Linux Secret Service\n * (D-Bus / GNOME Keyring).\n *\n * Use this for OAuth refresh tokens, API keys, login passwords —\n * anything you'd be uncomfortable storing in `localStorage`. Items\n * are scoped under a `service` namespace, typically your app's\n * bundle identifier.\n *\n * No web fallback. The whole point is OS-protected storage; falling\n * back to localStorage would silently downgrade security guarantees.\n * Calls outside a Craft window throw.\n */\nimport { requireBridge } from './_bridge'\n\nexport interface KeychainAPI {\n /**\n * Store a secret. Overwrites any existing entry under (service, account).\n * The password may be any UTF-8 string.\n */\n set: (service: string, account: string, password: string) => Promise<void>\n /**\n * Read a secret. Returns `null` (not undefined) if no entry exists,\n * so callers can distinguish \"not found\" from \"found, value is empty\".\n */\n get: (service: string, account: string) => Promise<string | null>\n /** Delete a secret. No-op if it doesn't exist. */\n delete: (service: string, account: string) => Promise<void>\n /** Check whether a secret exists, without reading it (no decrypt cost). */\n has: (service: string, account: string) => Promise<boolean>\n}\n\nexport const keychain: KeychainAPI = {\n async set(service, account, password) {\n if (!service) throw new Error('keychain.set: service is required')\n if (!account) throw new Error('keychain.set: account is required')\n await requireBridge('keychain').set(service, account, password)\n },\n async get(service, account) {\n if (!service) throw new Error('keychain.get: service is required')\n if (!account) throw new Error('keychain.get: account is required')\n const v = await requireBridge('keychain').get(service, account)\n return typeof v === 'string' ? v : null\n },\n async delete(service, account) {\n if (!service) throw new Error('keychain.delete: service is required')\n if (!account) throw new Error('keychain.delete: account is required')\n await requireBridge('keychain').delete(service, account)\n },\n async has(service, account) {\n if (!service) throw new Error('keychain.has: service is required')\n if (!account) throw new Error('keychain.has: account is required')\n return await requireBridge('keychain').has(service, account)\n },\n}\n",
36
- "/**\n * Privacy Permissions\n *\n * Check and request OS-level permission for sensitive capabilities\n * (camera, microphone, screen recording, etc). Wraps macOS TCC\n * (`AVCaptureDevice authorizationStatusForMediaType:`) and equivalents.\n *\n * Browser fallback uses the (limited) `navigator.permissions.query`\n * API where available — note that the web API only knows about a small\n * subset of names ('camera', 'microphone', 'geolocation', 'notifications').\n */\nimport { hasBridge } from './_bridge'\n\n/** Status values match the macOS TCC convention. */\nexport type PermissionStatus = 'granted' | 'denied' | 'restricted' | 'undetermined' | 'not-supported'\n\nexport type PermissionName = \n| 'camera'\n| 'microphone'\n| 'screen_recording'\n| 'accessibility'\n| 'full_disk_access'\n| 'input_monitoring'\n| 'location'\n| 'notifications'\n| 'contacts'\n| 'calendar'\n| 'reminders'\n| 'photos'\n| 'bluetooth'\n\nexport interface PermissionsAPI {\n /** Read current status without prompting. */\n check: (name: PermissionName) => Promise<PermissionStatus>\n /**\n * Request permission. On macOS this triggers the system modal for\n * permissions that haven't been answered yet; for ones already in\n * a non-undetermined state, the user has to flip it manually in\n * System Settings — call `openSettings(name)` to jump them there.\n */\n request: (name: PermissionName) => Promise<PermissionStatus>\n /** Open the Privacy pane scoped to the named permission. */\n openSettings: (name?: PermissionName) => Promise<void>\n}\n\nexport const permissions: PermissionsAPI = {\n async check(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.check(name)\n return await webCheck(name)\n },\n async request(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.request(name)\n return await webRequest(name)\n },\n async openSettings(name) {\n if (hasBridge('permissions')) await window.craft!.permissions.openSettings(name)\n },\n}\n\nasync function webCheck(name: PermissionName): Promise<PermissionStatus> {\n if (typeof navigator === 'undefined' || !(navigator as any).permissions?.query) return 'not-supported'\n try {\n const result = await (navigator as any).permissions.query({ name })\n return mapWebState(result.state)\n }\n catch { return 'not-supported' }\n}\n\nasync function webRequest(name: PermissionName): Promise<PermissionStatus> {\n // The web has no general-purpose `request` — most APIs prompt\n // implicitly when you try to use them. We special-case the well-known\n // ones and otherwise fall back to a `check`.\n if (name === 'notifications' && typeof window !== 'undefined' && 'Notification' in window) {\n const r = await (window as any).Notification.requestPermission()\n return r === 'granted' ? 'granted' : r === 'denied' ? 'denied' : 'undetermined'\n }\n return await webCheck(name)\n}\n\nfunction mapWebState(s: string): PermissionStatus {\n if (s === 'granted') return 'granted'\n if (s === 'denied') return 'denied'\n if (s === 'prompt') return 'undetermined'\n return 'undetermined'\n}\n",
36
+ "/**\n * Privacy Permissions\n *\n * Check and request OS-level permission for sensitive capabilities\n * (camera, microphone, screen recording, etc). Wraps macOS TCC\n * (`AVCaptureDevice authorizationStatusForMediaType:`) and equivalents.\n *\n * Browser fallback uses the (limited) `navigator.permissions.query`\n * API where available — note that the web API only knows about a small\n * subset of names ('camera', 'microphone', 'geolocation', 'notifications').\n */\nimport { hasBridge } from './_bridge'\n\n/** Status values match the macOS TCC convention. */\nexport type PermissionStatus = 'granted' | 'denied' | 'restricted' | 'undetermined' | 'not-supported'\n\nexport type PermissionName =\n| 'camera'\n| 'microphone'\n| 'screen_recording'\n| 'accessibility'\n| 'full_disk_access'\n| 'input_monitoring'\n| 'location'\n| 'notifications'\n| 'contacts'\n| 'calendar'\n| 'reminders'\n| 'photos'\n| 'bluetooth'\n\nexport interface PermissionsAPI {\n /** Read current status without prompting. */\n check: (name: PermissionName) => Promise<PermissionStatus>\n /**\n * Request permission. On macOS this triggers the system modal for\n * permissions that haven't been answered yet; for ones already in\n * a non-undetermined state, the user has to flip it manually in\n * System Settings — call `openSettings(name)` to jump them there.\n */\n request: (name: PermissionName) => Promise<PermissionStatus>\n /** Open the Privacy pane scoped to the named permission. */\n openSettings: (name?: PermissionName) => Promise<void>\n}\n\nexport const permissions: PermissionsAPI = {\n async check(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.check(name)\n return await webCheck(name)\n },\n async request(name) {\n if (hasBridge('permissions')) return await window.craft!.permissions.request(name)\n return await webRequest(name)\n },\n async openSettings(name) {\n if (hasBridge('permissions')) await window.craft!.permissions.openSettings(name)\n },\n}\n\nasync function webCheck(name: PermissionName): Promise<PermissionStatus> {\n if (typeof navigator === 'undefined' || !(navigator as any).permissions?.query) return 'not-supported'\n try {\n const result = await (navigator as any).permissions.query({ name })\n return mapWebState(result.state)\n }\n catch { return 'not-supported' }\n}\n\nasync function webRequest(name: PermissionName): Promise<PermissionStatus> {\n // The web has no general-purpose `request` — most APIs prompt\n // implicitly when you try to use them. We special-case the well-known\n // ones and otherwise fall back to a `check`.\n if (name === 'notifications' && typeof window !== 'undefined' && 'Notification' in window) {\n const r = await (window as any).Notification.requestPermission()\n return r === 'granted' ? 'granted' : r === 'denied' ? 'denied' : 'undetermined'\n }\n return await webCheck(name)\n}\n\nfunction mapWebState(s: string): PermissionStatus {\n if (s === 'granted') return 'granted'\n if (s === 'denied') return 'denied'\n if (s === 'prompt') return 'undetermined'\n return 'undetermined'\n}\n",
37
37
  "/**\n * Printing\n *\n * Print the current page (system print sheet) or save it as a PDF.\n * Uses `[WKWebView printOperationWithPrintInfo:]` on macOS — i.e.\n * the same code path Safari uses.\n *\n * Browser fallback uses `window.print()` for `print()`. There's no\n * portable web equivalent for \"silently save current page as PDF\" —\n * that path throws.\n */\nimport { hasBridge, requireBridge } from './_bridge'\n\nexport interface PrintToPDFResult {\n ok: boolean\n /** Where the PDF was written. */\n path?: string\n}\n\nexport interface PrintingAPI {\n /** Open the system print sheet for the current webview. */\n print: () => Promise<void>\n /** Render the current page to a PDF on disk. Path must be absolute. */\n printToPDF: (path: string) => Promise<PrintToPDFResult>\n}\n\nexport const printing: PrintingAPI = {\n async print() {\n if (hasBridge('printing')) {\n await window.craft!.printing.print()\n return\n }\n if (typeof window !== 'undefined' && typeof window.print === 'function') {\n window.print()\n }\n },\n\n async printToPDF(path: string): Promise<PrintToPDFResult> {\n if (!path) throw new Error('printToPDF: path is required')\n // Earlier we only accepted POSIX-style absolute paths (starts with\n // `/`), which broke on Windows where `C:\\Users\\...` is the norm.\n // Accept either: POSIX `/` prefix OR Windows drive-letter prefix\n // (`X:\\` or `X:/`). UNC paths (`\\\\server\\share`) also welcome.\n const isPosixAbs = path.startsWith('/')\n const isWinAbs = /^[a-zA-Z]:[\\\\/]/.test(path) || path.startsWith('\\\\\\\\')\n if (!isPosixAbs && !isWinAbs) {\n throw new Error('printToPDF: path must be absolute')\n }\n const r = await requireBridge('printing').printToPDF(path)\n return { ok: !!(r && r.ok), path: r?.path }\n },\n}\n",
38
38
  "/**\n * Native Auto-Launch (start at login)\n *\n * Tells the OS to launch the app automatically when the user signs in.\n * Backed by `SMAppService` on macOS Ventura+. **Different from\n * `autolaunch.ts`** — that older module shells out to subprocesses\n * (`osascript`, etc); this one uses the modern Apple API.\n *\n * For a clean migration: prefer this module for new code. Keep\n * `autolaunch.ts` for backward compatibility with apps that haven't\n * adopted the Craft bridge yet.\n *\n * On systems where SMAppService isn't available (older macOS, Linux,\n * Windows) `enable`/`disable` resolve to `false` — let the caller\n * fall back to the legacy module.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface NativeAutoLaunchAPI {\n /** Register the app to launch at login. Resolves to true on success. */\n enable: () => Promise<boolean>\n /** Unregister. Resolves to true on success. */\n disable: () => Promise<boolean>\n /** True if currently registered. */\n isEnabled: () => Promise<boolean>\n}\n\nexport const nativeAutoLaunch: NativeAutoLaunchAPI = {\n async enable() {\n if (!hasBridge('autoLaunch')) return false\n return await window.craft!.autoLaunch.enable()\n },\n async disable() {\n if (!hasBridge('autoLaunch')) return false\n return await window.craft!.autoLaunch.disable()\n },\n async isEnabled() {\n if (!hasBridge('autoLaunch')) return false\n return await window.craft!.autoLaunch.isEnabled()\n },\n}\n",
39
39
  "/**\n * Touch Bar (legacy macOS hardware)\n *\n * Apple has stopped shipping Touch Bar on new MacBooks, but a large\n * fleet of 2016-2022 Pros still has them. Apps that want to do the\n * right thing for those users can hand a small set of contextual\n * controls here.\n *\n * No fallback outside Craft windows — Touch Bar is hardware-only.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type TouchBarItemType = 'button' | 'label' | 'slider' | 'spacer' | 'colorPicker' | 'segmented'\n\nexport interface TouchBarItem {\n /** Unique id for later updates / event correlation. */\n id: string\n type: TouchBarItemType\n label?: string\n /** SF Symbol name or asset id. */\n icon?: string\n /** Initial state for sliders. 0..1. */\n value?: number\n /** Initial enabled state. */\n enabled?: boolean\n /** For segmented controls. */\n segments?: Array<{ id: string, label?: string, icon?: string }>\n}\n\nexport interface TouchBarActionEvent {\n /** id of the item the user touched. */\n id: string\n /** Slider value if applicable. */\n value?: number\n /** Selected segment id if applicable. */\n segment?: string\n}\n\nexport interface TouchBarAPI {\n addItem: (item: TouchBarItem) => Promise<void>\n removeItem: (id: string) => Promise<void>\n updateItem: (id: string, props: Partial<TouchBarItem>) => Promise<void>\n setLabel: (id: string, label: string) => Promise<void>\n setIcon: (id: string, icon: string) => Promise<void>\n setEnabled: (id: string, enabled: boolean) => Promise<void>\n setSliderValue: (id: string, value: number) => Promise<void>\n clear: () => Promise<void>\n show: () => Promise<void>\n hide: () => Promise<void>\n onAction: (cb: (event: TouchBarActionEvent) => void) => () => void\n}\n\nexport const touchbar: TouchBarAPI = {\n async addItem(item) { if (hasBridge('touchbar')) await window.craft!.touchbar.addItem(item) },\n async removeItem(id) { if (hasBridge('touchbar')) await window.craft!.touchbar.removeItem(id) },\n async updateItem(id, props) { if (hasBridge('touchbar')) await window.craft!.touchbar.updateItem(id, props) },\n async setLabel(id, label) { if (hasBridge('touchbar')) await window.craft!.touchbar.setLabel(id, label) },\n async setIcon(id, icon) { if (hasBridge('touchbar')) await window.craft!.touchbar.setIcon(id, icon) },\n async setEnabled(id, enabled) { if (hasBridge('touchbar')) await window.craft!.touchbar.setEnabled(id, enabled) },\n async setSliderValue(id, value) { if (hasBridge('touchbar')) await window.craft!.touchbar.setSliderValue(id, value) },\n async clear() { if (hasBridge('touchbar')) await window.craft!.touchbar.clear() },\n async show() { if (hasBridge('touchbar')) await window.craft!.touchbar.show() },\n async hide() { if (hasBridge('touchbar')) await window.craft!.touchbar.hide() },\n onAction(cb) {\n return onCraftEvent<TouchBarActionEvent>('craft:touchbar:action', cb)\n },\n}\n",
40
40
  "/**\n * Bluetooth\n *\n * Discover, pair, and connect to nearby devices. Uses CoreBluetooth on\n * macOS (`CBCentralManager`); equivalents on Linux/Windows.\n *\n * No web fallback — `navigator.bluetooth.requestDevice()` exists but\n * the model is fundamentally different (per-call user prompt, no\n * persistent pairings). Calls outside Craft return defaults.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface BluetoothDevice {\n /** Stable identifier. */\n id: string\n /** Friendly device name. May be empty. */\n name: string\n /** True if currently connected. */\n connected: boolean\n /** RSSI (signal strength, dBm). Negative — closer to 0 = stronger. */\n rssi?: number\n /** Manufacturer-data when present. */\n manufacturer?: string\n}\n\nexport type BluetoothPowerState = 'unknown' | 'resetting' | 'unsupported' | 'unauthorized' | 'poweredOff' | 'poweredOn'\n\nexport interface BluetoothService {\n /** UUID string (16- or 128-bit). CoreBluetooth normalizes both forms. */\n uuid: string\n /** True for \"primary\" services (vs \"included\" sub-services). */\n primary?: boolean\n}\n\nexport interface BluetoothCharacteristic {\n uuid: string\n /** Service this characteristic belongs to. */\n serviceUuid: string\n /** Properties advertised by the peripheral. */\n properties?: Array<'read' | 'write' | 'writeWithoutResponse' | 'notify' | 'indicate' | 'broadcast' | 'authenticatedSignedWrites' | 'extendedProperties'>\n}\n\nexport interface BluetoothCharacteristicValueEvent {\n deviceId: string\n serviceUuid: string\n characteristicUuid: string\n /** Hex-encoded bytes. */\n valueHex: string\n}\n\nexport type BluetoothWriteMode = 'with-response' | 'without-response'\n\nexport interface BluetoothAPI {\n isEnabled: () => Promise<boolean>\n powerState: () => Promise<BluetoothPowerState>\n connectedDevices: () => Promise<BluetoothDevice[]>\n pairedDevices: () => Promise<BluetoothDevice[]>\n startDiscovery: () => Promise<void>\n stopDiscovery: () => Promise<void>\n isDiscovering: () => Promise<boolean>\n connect: (id: string) => Promise<void>\n disconnect: (id: string) => Promise<void>\n openPreferences: () => Promise<void>\n /**\n * Discover GATT services on a connected peripheral. Resolves once the\n * native side returns the service list — does not stream incrementally.\n */\n discoverServices: (deviceId: string) => Promise<BluetoothService[]>\n /** Discover characteristics for one service. */\n discoverCharacteristics: (deviceId: string, serviceUuid: string) => Promise<BluetoothCharacteristic[]>\n /**\n * Read the current value of a characteristic. Returns hex-encoded bytes\n * — keeps the bridge JSON-friendly without forcing an opinion about\n * encoding (UTF-8 / float32 / etc.) at this layer.\n */\n readCharacteristic: (deviceId: string, serviceUuid: string, characteristicUuid: string) => Promise<{ ok: boolean, valueHex?: string, reason?: string }>\n /**\n * Write a value to a characteristic. `valueHex` should be a hex string\n * (`\"01ff\"`); `mode` defaults to `with-response` (CBCharacteristicWriteWithResponse).\n */\n writeCharacteristic: (\n deviceId: string,\n serviceUuid: string,\n characteristicUuid: string,\n valueHex: string,\n mode?: BluetoothWriteMode,\n ) => Promise<{ ok: boolean, reason?: string }>\n /**\n * Subscribe to value notifications/indications. Pass `false` to stop.\n * Updates arrive via `onCharacteristicValue`.\n */\n setCharacteristicNotify: (deviceId: string, serviceUuid: string, characteristicUuid: string, on: boolean) => Promise<{ ok: boolean, reason?: string }>\n /** Fired when a new device is discovered. */\n onDeviceFound: (cb: (device: BluetoothDevice) => void) => () => void\n /** Fired when a device finishes connecting. */\n onDeviceConnected: (cb: (device: BluetoothDevice) => void) => () => void\n /** Fired when a connected device disconnects. */\n onDeviceDisconnected: (cb: (device: BluetoothDevice) => void) => () => void\n /**\n * Fired when a subscribed characteristic delivers a new value. Tied to\n * `setCharacteristicNotify(..., true)`.\n */\n onCharacteristicValue: (cb: (event: BluetoothCharacteristicValueEvent) => void) => () => void\n}\n\nconst HEX_RE = /^[\\da-f]*$/i\n\nfunction assertHex(label: string, hex: string): void {\n if (typeof hex !== 'string' || !HEX_RE.test(hex) || hex.length % 2 !== 0) {\n throw new Error(`${label}: must be a hex string with even length, got ${JSON.stringify(hex)}`)\n }\n}\n\nexport const bluetooth: BluetoothAPI = {\n async isEnabled() { return hasBridge('bluetooth') ? await window.craft!.bluetooth.isEnabled() : false },\n async powerState() { return hasBridge('bluetooth') ? await window.craft!.bluetooth.powerState() : 'unknown' as BluetoothPowerState },\n async connectedDevices() { return hasBridge('bluetooth') ? await window.craft!.bluetooth.connectedDevices() : [] },\n async pairedDevices() { return hasBridge('bluetooth') ? await window.craft!.bluetooth.pairedDevices() : [] },\n async startDiscovery() { if (hasBridge('bluetooth')) await window.craft!.bluetooth.startDiscovery() },\n async stopDiscovery() { if (hasBridge('bluetooth')) await window.craft!.bluetooth.stopDiscovery() },\n async isDiscovering() { return hasBridge('bluetooth') ? await window.craft!.bluetooth.isDiscovering() : false },\n async connect(id) { if (hasBridge('bluetooth')) await window.craft!.bluetooth.connect(id) },\n async disconnect(id) { if (hasBridge('bluetooth')) await window.craft!.bluetooth.disconnect(id) },\n async openPreferences() { if (hasBridge('bluetooth')) await window.craft!.bluetooth.openPreferences() },\n\n async discoverServices(deviceId) {\n if (!deviceId) throw new Error('bluetooth.discoverServices: deviceId is required')\n if (!hasBridge('bluetooth')) return []\n const r = await window.craft!.bluetooth.discoverServices(deviceId)\n return Array.isArray(r) ? r as BluetoothService[] : []\n },\n\n async discoverCharacteristics(deviceId, serviceUuid) {\n if (!deviceId || !serviceUuid) throw new Error('bluetooth.discoverCharacteristics: deviceId and serviceUuid are required')\n if (!hasBridge('bluetooth')) return []\n const r = await window.craft!.bluetooth.discoverCharacteristics(deviceId, serviceUuid)\n return Array.isArray(r) ? r as BluetoothCharacteristic[] : []\n },\n\n async readCharacteristic(deviceId, serviceUuid, characteristicUuid) {\n if (!deviceId || !serviceUuid || !characteristicUuid) {\n throw new Error('bluetooth.readCharacteristic: deviceId, serviceUuid, characteristicUuid are required')\n }\n if (!hasBridge('bluetooth')) return { ok: false, reason: 'bridge unavailable' }\n return await window.craft!.bluetooth.readCharacteristic(deviceId, serviceUuid, characteristicUuid)\n },\n\n async writeCharacteristic(deviceId, serviceUuid, characteristicUuid, valueHex, mode = 'with-response') {\n if (!deviceId || !serviceUuid || !characteristicUuid) {\n throw new Error('bluetooth.writeCharacteristic: deviceId, serviceUuid, characteristicUuid are required')\n }\n assertHex('bluetooth.writeCharacteristic.valueHex', valueHex)\n if (!hasBridge('bluetooth')) return { ok: false, reason: 'bridge unavailable' }\n return await window.craft!.bluetooth.writeCharacteristic(deviceId, serviceUuid, characteristicUuid, valueHex, mode)\n },\n\n async setCharacteristicNotify(deviceId, serviceUuid, characteristicUuid, on) {\n if (!deviceId || !serviceUuid || !characteristicUuid) {\n throw new Error('bluetooth.setCharacteristicNotify: deviceId, serviceUuid, characteristicUuid are required')\n }\n if (!hasBridge('bluetooth')) return { ok: false, reason: 'bridge unavailable' }\n return await window.craft!.bluetooth.setCharacteristicNotify(deviceId, serviceUuid, characteristicUuid, on)\n },\n\n onDeviceFound(cb) { return onCraftEvent<BluetoothDevice>('craft:bluetooth:deviceFound', cb) },\n onDeviceConnected(cb) { return onCraftEvent<BluetoothDevice>('craft:bluetooth:deviceConnected', cb) },\n onDeviceDisconnected(cb) { return onCraftEvent<BluetoothDevice>('craft:bluetooth:deviceDisconnected', cb) },\n onCharacteristicValue(cb) {\n return onCraftEvent<BluetoothCharacteristicValueEvent>('craft:bluetooth:characteristicValue', cb)\n },\n}\n",
41
41
  "/**\n * Text-to-Speech\n *\n * Speak text aloud through the system synthesizer. macOS uses\n * `AVSpeechSynthesizer` (the modern, system-wide voice that respects\n * the user's preferred voice + rate).\n *\n * Browser fallback uses `window.speechSynthesis` — same shape, similar\n * voice list. The two paths are deliberately compatible so call sites\n * don't need to branch.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface SpeakOptions {\n /** Voice identifier (e.g. `com.apple.voice.compact.en-US.Samantha`)\n * or BCP-47 language tag (`en-US`). Optional — system default used. */\n voice?: string\n /** Rate, 0..1. AVSpeechUtterance treats ~0.5 as natural. */\n rate?: number\n /** Pitch multiplier, 0.5..2.0. Default 1.0. */\n pitch?: number\n /** Volume, 0..1. Default 1.0. */\n volume?: number\n}\n\nexport interface SpeechVoice {\n /** Stable identifier for `voice` option. */\n id: string\n /** Human-readable voice name (e.g. \"Samantha\"). */\n name: string\n /** BCP-47 language code. */\n language: string\n /** \"default\" or \"enhanced\" (premium installed voice). */\n quality: 'default' | 'enhanced'\n}\n\nexport interface SpeechAPI {\n /** Speak the given text. Returns once the utterance is *queued*, not finished. */\n speak: (text: string, options?: SpeakOptions) => Promise<void>\n /** Stop speaking immediately. */\n stop: () => Promise<void>\n /** Pause at the next word boundary. Resume with `resume()`. */\n pause: () => Promise<void>\n /** Resume a paused utterance. */\n resume: () => Promise<void>\n /** True if the synthesizer currently has an utterance. */\n isSpeaking: () => Promise<boolean>\n /** All installed voices on the system. */\n getVoices: () => Promise<SpeechVoice[]>\n}\n\nexport const speech: SpeechAPI = {\n async speak(text, options) {\n if (!text) throw new Error('speech.speak: text is required')\n if (hasBridge('speech')) {\n await window.craft!.speech.speak(text, options)\n return\n }\n if (typeof window !== 'undefined' && (window as any).speechSynthesis) {\n const u = new (window as any).SpeechSynthesisUtterance(text)\n if (options) {\n if (options.rate != null) u.rate = options.rate\n if (options.pitch != null) u.pitch = options.pitch\n if (options.volume != null) u.volume = options.volume\n if (options.voice) {\n // Web SpeechSynthesisVoice is matched by name OR voiceURI;\n // try both to be permissive.\n const voices = (window as any).speechSynthesis.getVoices()\n const match = voices.find((v: any) => v.voiceURI === options.voice || v.name === options.voice || v.lang === options.voice)\n if (match) u.voice = match\n }\n }\n ;(window as any).speechSynthesis.speak(u)\n }\n },\n\n async stop() {\n if (hasBridge('speech')) {\n await window.craft!.speech.stop()\n return\n }\n if (typeof window !== 'undefined' && (window as any).speechSynthesis) {\n ;(window as any).speechSynthesis.cancel()\n }\n },\n\n async pause() {\n if (hasBridge('speech')) {\n await window.craft!.speech.pause()\n return\n }\n if (typeof window !== 'undefined' && (window as any).speechSynthesis) {\n ;(window as any).speechSynthesis.pause()\n }\n },\n\n async resume() {\n if (hasBridge('speech')) {\n await window.craft!.speech.resume()\n return\n }\n if (typeof window !== 'undefined' && (window as any).speechSynthesis) {\n ;(window as any).speechSynthesis.resume()\n }\n },\n\n async isSpeaking() {\n if (hasBridge('speech')) return await window.craft!.speech.isSpeaking()\n if (typeof window !== 'undefined' && (window as any).speechSynthesis) {\n return !!(window as any).speechSynthesis.speaking\n }\n return false\n },\n\n async getVoices() {\n if (hasBridge('speech')) return await window.craft!.speech.getVoices()\n if (typeof window !== 'undefined' && (window as any).speechSynthesis) {\n const raw = (window as any).speechSynthesis.getVoices() as any[]\n return raw.map(v => ({\n id: v.voiceURI || v.name,\n name: v.name,\n language: v.lang || '',\n quality: 'default' as const,\n }))\n }\n return []\n },\n}\n",
42
42
  "/**\n * Crash Reporter\n *\n * Capture unhandled exceptions and queue them for forwarding to a\n * backend of your choice. The native side stores up to 64 most-recent\n * entries in memory (ring buffer); call `flush()` periodically to\n * drain and forward.\n *\n * **Why the design**: we deliberately don't ship a built-in HTTP\n * uploader. Apps care strongly about WHERE crash reports go (data\n * residency, privacy disclosures, retention) — picking a default\n * would surprise users. `flush()` returns the queue, `clear()` empties\n * it; that's enough to compose any backend on top.\n *\n * Browser fallback: keeps a JS-side queue with the same shape so\n * call sites don't branch. No automatic upload either way.\n */\nimport { hasBridge } from './_bridge'\n\nexport type CrashSeverity = 'fatal' | 'error' | 'warning'\n\nexport interface CrashReport {\n severity?: CrashSeverity\n message: string\n /** Where the crash originated. Defaults to `'js'`. */\n source?: 'js' | 'native'\n /** Stack trace, if available. */\n stack?: string\n}\n\nexport interface StoredCrashEntry {\n timestamp: number\n severity: string\n message: string\n source: string\n stack: string\n userId?: string\n appVersion?: string\n}\n\nexport interface CrashForwarderOptions {\n /** Where to POST the JSON payload. */\n endpoint: string\n /**\n * How often to drain the queue, in ms. Default: 60_000 (one minute).\n * Set to 0 to disable the timer (you'll call `flushNow()` yourself).\n */\n intervalMs?: number\n /**\n * Optional shared secret. If supplied, every POST gets an\n * `X-Craft-Signature: hex(HMAC-SHA256(secret, body))` header so the\n * receiver can authenticate the report.\n */\n signingSecret?: string\n /**\n * PII redaction. Defaults to `true` — strips emails, IPv4 addresses,\n * and absolute home paths from `message` and `stack`. Pass `false`\n * to send raw, or a custom function to do your own scrubbing.\n */\n redact?: boolean | ((entry: StoredCrashEntry) => StoredCrashEntry)\n /**\n * Extra headers to merge onto every request (auth tokens, etc.).\n */\n headers?: Record<string, string>\n /**\n * How many retries to attempt before dropping a batch. Default: 5.\n * Backoff doubles each attempt starting from 1 second.\n */\n maxRetries?: number\n /**\n * Persist the pending queue under this `localStorage` key so reports\n * survive a webview reload. Default: `'craft:crashReporter:pending'`.\n * Pass `null` to disable persistence.\n */\n persistKey?: string | null\n}\n\nexport interface CrashForwarderHandle {\n /** Force a flush right now, bypassing the timer. */\n flushNow: () => Promise<void>\n /** Stop the timer and detach. The pending queue stays persisted. */\n stop: () => void\n /** Inspect what's currently waiting to be sent. */\n pending: () => StoredCrashEntry[]\n}\n\nexport interface CrashReporterAPI {\n /**\n * Report a crash. Accepts either a `CrashReport` object or a\n * native `Error` — both get normalized to the storage shape.\n */\n report: (entry: CrashReport | Error) => Promise<void>\n /** Drain the queue and return the entries. Doesn't clear the queue. */\n flush: () => Promise<StoredCrashEntry[]>\n /** Empty the queue. */\n clear: () => Promise<void>\n /** Toggle reporting on/off. When off, `report()` is a no-op. */\n setEnabled: (on: boolean) => Promise<void>\n isEnabled: () => Promise<boolean>\n /** Tag every subsequent crash with this user id. */\n setUser: (id: string) => Promise<void>\n /** Tag every subsequent crash with this app version. */\n setAppVersion: (version: string) => Promise<void>\n /**\n * One-shot setup: hook `window.onerror` and `unhandledrejection`\n * so every uncaught failure routes through `report()` automatically.\n * Returns an `off()` to detach the global handlers.\n */\n attachGlobalHandlers: () => () => void\n /**\n * Wire a periodic HTTP forwarder. Drains `flush()` on a timer and\n * POSTs `{ entries: [...] }` to `endpoint`. Failed requests retry\n * with exponential backoff; the unsent batch is persisted to\n * localStorage so a reload doesn't lose it. Returns a handle with\n * `stop()` and `flushNow()`.\n */\n forwardTo: (options: CrashForwarderOptions) => CrashForwarderHandle\n}\n\n// JS-side fallback queue when native isn't available.\nconst jsQueue: StoredCrashEntry[] = []\nlet jsEnabled = true\nlet jsUserId: string | undefined\nlet jsAppVersion: string | undefined\n\nexport const crashReporter: CrashReporterAPI = {\n async report(entry) {\n if (hasBridge('crashReporter')) {\n await window.craft!.crashReporter.report(entry)\n return\n }\n if (!jsEnabled) return\n const normalized: StoredCrashEntry = entry instanceof Error\n ? {\n timestamp: Date.now(),\n severity: 'error',\n message: entry.message,\n source: 'js',\n stack: entry.stack || '',\n userId: jsUserId,\n appVersion: jsAppVersion,\n }\n : {\n timestamp: Date.now(),\n severity: entry.severity || 'error',\n message: entry.message || '',\n source: entry.source || 'js',\n stack: entry.stack || '',\n userId: jsUserId,\n appVersion: jsAppVersion,\n }\n if (jsQueue.length >= 64) jsQueue.shift()\n jsQueue.push(normalized)\n },\n\n async flush() {\n if (hasBridge('crashReporter')) return await window.craft!.crashReporter.flush()\n return [...jsQueue]\n },\n\n async clear() {\n if (hasBridge('crashReporter')) {\n await window.craft!.crashReporter.clear()\n return\n }\n jsQueue.length = 0\n },\n\n async setEnabled(on) {\n if (hasBridge('crashReporter')) {\n await window.craft!.crashReporter.setEnabled(on)\n return\n }\n jsEnabled = on\n },\n\n async isEnabled() {\n if (hasBridge('crashReporter')) return await window.craft!.crashReporter.isEnabled()\n return jsEnabled\n },\n\n async setUser(id) {\n if (hasBridge('crashReporter')) {\n await window.craft!.crashReporter.setUser(id)\n return\n }\n jsUserId = id || undefined\n },\n\n async setAppVersion(version) {\n if (hasBridge('crashReporter')) {\n await window.craft!.crashReporter.setAppVersion(version)\n return\n }\n jsAppVersion = version || undefined\n },\n\n attachGlobalHandlers() {\n if (hasBridge('crashReporter') && window.craft!.crashReporter.attachGlobalHandlers) {\n return window.craft!.crashReporter.attachGlobalHandlers()\n }\n if (typeof window === 'undefined') return () => {}\n const errorH = (e: ErrorEvent) => {\n crashReporter.report({\n severity: 'error',\n message: e.message,\n source: 'js',\n stack: e.error?.stack || `${e.message}\\n at ${e.filename}:${e.lineno}:${e.colno}`,\n }).catch(() => {})\n }\n const rejectH = (e: PromiseRejectionEvent) => {\n const r: any = e.reason\n crashReporter.report({\n severity: 'error',\n message: r?.message || String(r),\n source: 'js',\n stack: r?.stack || '',\n }).catch(() => {})\n }\n window.addEventListener('error', errorH)\n window.addEventListener('unhandledrejection', rejectH)\n return () => {\n window.removeEventListener('error', errorH)\n window.removeEventListener('unhandledrejection', rejectH)\n }\n },\n\n forwardTo(options) {\n return startForwarder(options)\n },\n}\n\n// =============================================================================\n// HTTP Forwarder\n// =============================================================================\n\nconst DEFAULT_PERSIST_KEY = 'craft:crashReporter:pending'\n\nconst EMAIL_RE = /[\\w.+-]+@[\\w-]+\\.[\\w.-]+/g\nconst IPV4_RE = /\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b/g\nconst HOME_PATH_RE = /\\/(?:Users|home)\\/[^\\s/'\"`]+/g\n\n/** Default redactor — replaces emails, IPv4s, and home paths with placeholders. */\nexport function redactPII(entry: StoredCrashEntry): StoredCrashEntry {\n const scrub = (s: string): string => s\n .replace(EMAIL_RE, '<email>')\n .replace(IPV4_RE, '<ip>')\n .replace(HOME_PATH_RE, '/<home>')\n return {\n ...entry,\n message: scrub(entry.message),\n stack: scrub(entry.stack),\n }\n}\n\n/** Sign `body` with HMAC-SHA256 + `secret`. Returns lowercase hex. */\nexport async function signPayload(secret: string, body: string): Promise<string> {\n const enc = new TextEncoder()\n const key = await crypto.subtle.importKey(\n 'raw',\n enc.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n const sig = await crypto.subtle.sign('HMAC', key, enc.encode(body))\n return [...new Uint8Array(sig)].map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction loadPersisted(key: string | null | undefined): StoredCrashEntry[] {\n if (!key || typeof localStorage === 'undefined') return []\n try {\n const raw = localStorage.getItem(key)\n if (!raw) return []\n const parsed = JSON.parse(raw)\n return Array.isArray(parsed) ? parsed as StoredCrashEntry[] : []\n }\n catch {\n return []\n }\n}\n\nfunction savePersisted(key: string | null | undefined, entries: StoredCrashEntry[]): void {\n if (!key || typeof localStorage === 'undefined') return\n try {\n if (entries.length === 0) localStorage.removeItem(key)\n else localStorage.setItem(key, JSON.stringify(entries))\n }\n catch { /* quota / disabled — best effort */ }\n}\n\nfunction startForwarder(options: CrashForwarderOptions): CrashForwarderHandle {\n const {\n endpoint,\n intervalMs = 60_000,\n signingSecret,\n redact = true,\n headers = {},\n maxRetries = 5,\n persistKey,\n } = options\n\n // Default the persist key only when the caller didn't explicitly opt out.\n // `persistKey === null` disables persistence; `undefined` falls back to\n // the standard key.\n const storageKey: string | null = persistKey === null\n ? null\n : (persistKey ?? DEFAULT_PERSIST_KEY)\n\n let pending: StoredCrashEntry[] = loadPersisted(storageKey)\n let stopped = false\n let timer: ReturnType<typeof setInterval> | null = null\n let inFlight: Promise<void> | null = null\n\n const redactor = typeof redact === 'function'\n ? redact\n : redact === false\n ? (e: StoredCrashEntry) => e\n : redactPII\n\n async function postBatch(batch: StoredCrashEntry[]): Promise<void> {\n const body = JSON.stringify({ entries: batch })\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...headers,\n }\n if (signingSecret) {\n requestHeaders['X-Craft-Signature'] = await signPayload(signingSecret, body)\n }\n\n let attempt = 0\n let delay = 1000\n while (!stopped) {\n try {\n const res = await fetch(endpoint, { method: 'POST', headers: requestHeaders, body })\n if (res.ok) return\n // 4xx — drop, since retrying would just fail again.\n if (res.status >= 400 && res.status < 500) return\n throw new Error(`HTTP ${res.status}`)\n }\n catch (err) {\n attempt += 1\n if (attempt > maxRetries) {\n // Re-queue at the front so the next tick retries from a fresh batch.\n pending = [...batch, ...pending]\n savePersisted(storageKey, pending)\n throw err\n }\n await new Promise<void>((r) => { setTimeout(r, delay) })\n delay = Math.min(delay * 2, 60_000)\n }\n }\n }\n\n async function drain(): Promise<void> {\n if (stopped || inFlight) return inFlight ?? undefined\n const fresh = await crashReporter.flush()\n if (fresh.length > 0) await crashReporter.clear()\n pending = [...pending, ...fresh.map(e => redactor(e))]\n if (pending.length === 0) return\n\n const batch = pending\n pending = []\n savePersisted(storageKey, pending)\n\n inFlight = postBatch(batch)\n .catch(() => { /* batch was already requeued in postBatch */ })\n .finally(() => { inFlight = null })\n return inFlight\n }\n\n if (intervalMs > 0) {\n timer = setInterval(() => { drain().catch(() => {}) }, intervalMs)\n }\n\n return {\n async flushNow() { await drain() },\n stop() {\n stopped = true\n if (timer) clearInterval(timer)\n timer = null\n },\n pending() { return [...pending] },\n }\n}\n",
43
- "/**\n * In-App Purchases (StoreKit on macOS)\n *\n * **Scope**: minimal. Full StoreKit support involves SKProductsRequest\n * delegates, transaction observers, receipt validation, family sharing,\n * promotional offers — months of work. The native side currently\n * implements `isAvailable`, `restorePurchases`, and `getReceiptData`\n * fully; `getProducts`, `purchase`, and `finishTransaction` are stubs\n * pending the StoreKit observer wiring.\n *\n * Apps that need a working IAP flow today should:\n * 1. Use `getReceiptData()` to grab the bundled App Store receipt\n * and verify it server-side via Apple's verifyReceipt API.\n * 2. Implement product fetch + purchase via a separate SDK or\n * direct StoreKit code in your bundle.\n *\n * This shape is intentionally stable so apps can write against it now\n * and switch over to a richer implementation when it lands.\n */\nimport { hasBridge } from './_bridge'\n\nexport type IAPProductType = \n| 'consumable'\n| 'non-consumable'\n| 'auto-subscription'\n| 'non-auto-subscription'\n\nexport type IAPSubscriptionPeriod = 'day' | 'week' | 'month' | 'year'\n\nexport interface IAPSubscriptionInfo {\n /** Length of one period — `1` + `month` means a one-month subscription. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** Free trial / intro offer attached to this subscription, if any. */\n introductoryOffer?: IAPIntroductoryOffer\n /** True when the subscription is shareable with iCloud Family. */\n familyShareable?: boolean\n /** Subscription group identifier. Used by StoreKit for upgrade/downgrade math. */\n groupIdentifier?: string\n}\n\nexport interface IAPIntroductoryOffer {\n /** \"free-trial\" | \"pay-as-you-go\" | \"pay-up-front\". */\n paymentMode: 'free-trial' | 'pay-as-you-go' | 'pay-up-front'\n /** Localized price including currency symbol; `\"0.00\"` for free trials. */\n localizedPrice: string\n /** How long the intro lasts. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** How many billing periods the intro repeats over (>=1). */\n numberOfPeriods?: number\n}\n\nexport interface IAPProduct {\n id: string\n title: string\n description?: string\n price: string\n /** Currency code (e.g. \"USD\"). */\n currency?: string\n /** Localized price including currency symbol. */\n localizedPrice?: string\n type?: IAPProductType\n /** Present only when `type === 'auto-subscription'`. */\n subscription?: IAPSubscriptionInfo\n}\n\nexport interface IAPPurchaseResult {\n /** True when the purchase was queued successfully. */\n queued: boolean\n productId?: string\n /** Why the purchase couldn't be queued (transient stub limitation, etc). */\n reason?: string\n}\n\nexport interface IAPTransactionEvent {\n productId: string\n transactionId: string\n /** ISO date string. */\n date?: string\n /** Original transaction id — populated for renewals / restores. */\n originalTransactionId?: string\n /** True when this purchase was issued under iCloud Family Sharing. */\n familyShared?: boolean\n /** True when restoring a previously purchased non-consumable. */\n restored?: boolean\n /** Subscription auto-renewal status, if applicable. */\n autoRenewing?: boolean\n /** ISO date when the current subscription period expires. */\n expiresAt?: string\n /** True when StoreKit reports this transaction is in the intro/free-trial period. */\n inIntroPeriod?: boolean\n}\n\nexport interface IAPFailureEvent {\n productId: string\n /** Apple's SKErrorCode value. */\n code?: number\n message?: string\n}\n\nexport interface IAPRefundEvent {\n productId: string\n transactionId: string\n /** ISO date when the refund was issued. */\n refundedAt?: string\n /** \"voluntary\" | \"issue-app\" | \"other\" — Apple's refund preference. */\n reason?: string\n}\n\nexport interface IAPSubscriptionStatusEvent {\n productId: string\n /** \"active\" | \"expired\" | \"in-grace-period\" | \"in-billing-retry\" | \"revoked\". */\n status: 'active' | 'expired' | 'in-grace-period' | 'in-billing-retry' | 'revoked'\n /** ISO date when status takes effect. */\n changedAt?: string\n /** ISO date the current period ends. */\n expiresAt?: string\n}\n\nexport interface IAPAPI {\n /** True if the device is allowed to make payments. */\n isAvailable: () => Promise<boolean>\n /** Fetch products by id. May return an empty array if native fetch isn't wired yet. */\n getProducts: (ids: string[] | string) => Promise<IAPProduct[]>\n /** Queue a purchase. Result fires via `onPurchased` / `onFailed`. */\n purchase: (productId: string) => Promise<IAPPurchaseResult>\n /** Restore previously-bought non-consumables tied to the user's Apple ID. */\n restorePurchases: () => Promise<{ ok: boolean }>\n /**\n * Mark a transaction as finished — required for non-consumables to\n * stop StoreKit re-delivering them.\n */\n finishTransaction: (transactionId: string) => Promise<void>\n /**\n * Read the App Store receipt as a base64 string. Hand this to your\n * server and call Apple's `verifyReceipt` for trusted validation.\n */\n getReceiptData: () => Promise<string | null>\n /** Subscribe to successful purchase events. */\n onPurchased: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to purchase-failure events. */\n onFailed: (cb: (e: IAPFailureEvent) => void) => () => void\n /** Subscribe to \"purchase restored\" events. */\n onRestored: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to async products-fetch results. */\n onProductsLoaded: (cb: (products: IAPProduct[]) => void) => () => void\n /**\n * Fires when a previous purchase has been refunded by the user via the\n * App Store. Apps should immediately revoke the entitlement granted by\n * the original transaction.\n */\n onRefunded: (cb: (e: IAPRefundEvent) => void) => () => void\n /**\n * Fires whenever a subscription's lifecycle status changes — the user\n * lapses out of grace period, billing retry resolves, etc.\n */\n onSubscriptionStatusChanged: (cb: (e: IAPSubscriptionStatusEvent) => void) => () => void\n /**\n * Returns the currently-active subscription product ids, plus their\n * status. Use on app boot to gate features rather than re-running\n * `restorePurchases()` every time.\n */\n getActiveSubscriptions: () => Promise<IAPSubscriptionStatusEvent[]>\n /**\n * True if the user is eligible for the introductory offer attached to\n * `productId`. Apple gates intro eligibility per subscription group:\n * if the user has ever subscribed to *any* product in the same group,\n * they're ineligible for further intro offers.\n */\n isEligibleForIntroOffer: (productId: string) => Promise<boolean>\n}\n\nimport { onCraftEvent } from './_bridge'\n\nexport const iap: IAPAPI = {\n async isAvailable() {\n if (!hasBridge('iap')) return false\n return await window.craft!.iap.isAvailable()\n },\n async getProducts(ids) {\n if (!hasBridge('iap')) return []\n // Normalize to array at the TS boundary so the bridge always sees\n // the same shape, regardless of whether the caller passed a\n // single id or a list.\n const arr = Array.isArray(ids) ? ids : [String(ids)]\n return await window.craft!.iap.getProducts(arr)\n },\n async purchase(productId) {\n if (!hasBridge('iap')) return { queued: false, productId, reason: 'IAP bridge not available' }\n const r = await window.craft!.iap.purchase(productId)\n return { queued: !!(r && r.queued), productId: r?.productId, reason: r?.reason }\n },\n async restorePurchases() {\n if (!hasBridge('iap')) return { ok: false }\n const r = await window.craft!.iap.restorePurchases()\n return { ok: !!(r && r.ok) }\n },\n async finishTransaction(transactionId) {\n if (!hasBridge('iap')) return\n await window.craft!.iap.finishTransaction(transactionId)\n },\n async getReceiptData() {\n if (!hasBridge('iap')) return null\n const r = await window.craft!.iap.getReceiptData()\n return r ? String(r) : null\n },\n onPurchased(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:purchased', cb) },\n onFailed(cb) { return onCraftEvent<IAPFailureEvent>('craft:iap:failed', cb) },\n onRestored(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:restored', cb) },\n onProductsLoaded(cb) {\n return onCraftEvent<{ products?: IAPProduct[] }>('craft:iap:productsLoaded', (e) => cb(e.products || []))\n },\n onRefunded(cb) { return onCraftEvent<IAPRefundEvent>('craft:iap:refunded', cb) },\n onSubscriptionStatusChanged(cb) {\n return onCraftEvent<IAPSubscriptionStatusEvent>('craft:iap:subscriptionStatusChanged', cb)\n },\n async getActiveSubscriptions() {\n if (!hasBridge('iap')) return []\n // Older bridges may not implement this — defensive default keeps the\n // call shape stable so callers can ship before the native side ships.\n const fn = window.craft!.iap.getActiveSubscriptions\n if (typeof fn !== 'function') return []\n const r = await fn()\n return Array.isArray(r) ? r as IAPSubscriptionStatusEvent[] : []\n },\n async isEligibleForIntroOffer(productId) {\n if (!hasBridge('iap')) return false\n const fn = window.craft!.iap.isEligibleForIntroOffer\n if (typeof fn !== 'function') return false\n return !!(await fn(productId))\n },\n}\n",
43
+ "/**\n * In-App Purchases (StoreKit on macOS)\n *\n * **Scope**: minimal. Full StoreKit support involves SKProductsRequest\n * delegates, transaction observers, receipt validation, family sharing,\n * promotional offers — months of work. The native side currently\n * implements `isAvailable`, `restorePurchases`, and `getReceiptData`\n * fully; `getProducts`, `purchase`, and `finishTransaction` are stubs\n * pending the StoreKit observer wiring.\n *\n * Apps that need a working IAP flow today should:\n * 1. Use `getReceiptData()` to grab the bundled App Store receipt\n * and verify it server-side via Apple's verifyReceipt API.\n * 2. Implement product fetch + purchase via a separate SDK or\n * direct StoreKit code in your bundle.\n *\n * This shape is intentionally stable so apps can write against it now\n * and switch over to a richer implementation when it lands.\n */\nimport { hasBridge } from './_bridge'\n\nexport type IAPProductType =\n| 'consumable'\n| 'non-consumable'\n| 'auto-subscription'\n| 'non-auto-subscription'\n\nexport type IAPSubscriptionPeriod = 'day' | 'week' | 'month' | 'year'\n\nexport interface IAPSubscriptionInfo {\n /** Length of one period — `1` + `month` means a one-month subscription. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** Free trial / intro offer attached to this subscription, if any. */\n introductoryOffer?: IAPIntroductoryOffer\n /** True when the subscription is shareable with iCloud Family. */\n familyShareable?: boolean\n /** Subscription group identifier. Used by StoreKit for upgrade/downgrade math. */\n groupIdentifier?: string\n}\n\nexport interface IAPIntroductoryOffer {\n /** \"free-trial\" | \"pay-as-you-go\" | \"pay-up-front\". */\n paymentMode: 'free-trial' | 'pay-as-you-go' | 'pay-up-front'\n /** Localized price including currency symbol; `\"0.00\"` for free trials. */\n localizedPrice: string\n /** How long the intro lasts. */\n numberOfUnits: number\n unit: IAPSubscriptionPeriod\n /** How many billing periods the intro repeats over (>=1). */\n numberOfPeriods?: number\n}\n\nexport interface IAPProduct {\n id: string\n title: string\n description?: string\n price: string\n /** Currency code (e.g. \"USD\"). */\n currency?: string\n /** Localized price including currency symbol. */\n localizedPrice?: string\n type?: IAPProductType\n /** Present only when `type === 'auto-subscription'`. */\n subscription?: IAPSubscriptionInfo\n}\n\nexport interface IAPPurchaseResult {\n /** True when the purchase was queued successfully. */\n queued: boolean\n productId?: string\n /** Why the purchase couldn't be queued (transient stub limitation, etc). */\n reason?: string\n}\n\nexport interface IAPTransactionEvent {\n productId: string\n transactionId: string\n /** ISO date string. */\n date?: string\n /** Original transaction id — populated for renewals / restores. */\n originalTransactionId?: string\n /** True when this purchase was issued under iCloud Family Sharing. */\n familyShared?: boolean\n /** True when restoring a previously purchased non-consumable. */\n restored?: boolean\n /** Subscription auto-renewal status, if applicable. */\n autoRenewing?: boolean\n /** ISO date when the current subscription period expires. */\n expiresAt?: string\n /** True when StoreKit reports this transaction is in the intro/free-trial period. */\n inIntroPeriod?: boolean\n}\n\nexport interface IAPFailureEvent {\n productId: string\n /** Apple's SKErrorCode value. */\n code?: number\n message?: string\n}\n\nexport interface IAPRefundEvent {\n productId: string\n transactionId: string\n /** ISO date when the refund was issued. */\n refundedAt?: string\n /** \"voluntary\" | \"issue-app\" | \"other\" — Apple's refund preference. */\n reason?: string\n}\n\nexport interface IAPSubscriptionStatusEvent {\n productId: string\n /** \"active\" | \"expired\" | \"in-grace-period\" | \"in-billing-retry\" | \"revoked\". */\n status: 'active' | 'expired' | 'in-grace-period' | 'in-billing-retry' | 'revoked'\n /** ISO date when status takes effect. */\n changedAt?: string\n /** ISO date the current period ends. */\n expiresAt?: string\n}\n\nexport interface IAPAPI {\n /** True if the device is allowed to make payments. */\n isAvailable: () => Promise<boolean>\n /** Fetch products by id. May return an empty array if native fetch isn't wired yet. */\n getProducts: (ids: string[] | string) => Promise<IAPProduct[]>\n /** Queue a purchase. Result fires via `onPurchased` / `onFailed`. */\n purchase: (productId: string) => Promise<IAPPurchaseResult>\n /** Restore previously-bought non-consumables tied to the user's Apple ID. */\n restorePurchases: () => Promise<{ ok: boolean }>\n /**\n * Mark a transaction as finished — required for non-consumables to\n * stop StoreKit re-delivering them.\n */\n finishTransaction: (transactionId: string) => Promise<void>\n /**\n * Read the App Store receipt as a base64 string. Hand this to your\n * server and call Apple's `verifyReceipt` for trusted validation.\n */\n getReceiptData: () => Promise<string | null>\n /** Subscribe to successful purchase events. */\n onPurchased: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to purchase-failure events. */\n onFailed: (cb: (e: IAPFailureEvent) => void) => () => void\n /** Subscribe to \"purchase restored\" events. */\n onRestored: (cb: (e: IAPTransactionEvent) => void) => () => void\n /** Subscribe to async products-fetch results. */\n onProductsLoaded: (cb: (products: IAPProduct[]) => void) => () => void\n /**\n * Fires when a previous purchase has been refunded by the user via the\n * App Store. Apps should immediately revoke the entitlement granted by\n * the original transaction.\n */\n onRefunded: (cb: (e: IAPRefundEvent) => void) => () => void\n /**\n * Fires whenever a subscription's lifecycle status changes — the user\n * lapses out of grace period, billing retry resolves, etc.\n */\n onSubscriptionStatusChanged: (cb: (e: IAPSubscriptionStatusEvent) => void) => () => void\n /**\n * Returns the currently-active subscription product ids, plus their\n * status. Use on app boot to gate features rather than re-running\n * `restorePurchases()` every time.\n */\n getActiveSubscriptions: () => Promise<IAPSubscriptionStatusEvent[]>\n /**\n * True if the user is eligible for the introductory offer attached to\n * `productId`. Apple gates intro eligibility per subscription group:\n * if the user has ever subscribed to *any* product in the same group,\n * they're ineligible for further intro offers.\n */\n isEligibleForIntroOffer: (productId: string) => Promise<boolean>\n}\n\nimport { onCraftEvent } from './_bridge'\n\nexport const iap: IAPAPI = {\n async isAvailable() {\n if (!hasBridge('iap')) return false\n return await window.craft!.iap.isAvailable()\n },\n async getProducts(ids) {\n if (!hasBridge('iap')) return []\n // Normalize to array at the TS boundary so the bridge always sees\n // the same shape, regardless of whether the caller passed a\n // single id or a list.\n const arr = Array.isArray(ids) ? ids : [String(ids)]\n return await window.craft!.iap.getProducts(arr)\n },\n async purchase(productId) {\n if (!hasBridge('iap')) return { queued: false, productId, reason: 'IAP bridge not available' }\n const r = await window.craft!.iap.purchase(productId)\n return { queued: !!(r && r.queued), productId: r?.productId, reason: r?.reason }\n },\n async restorePurchases() {\n if (!hasBridge('iap')) return { ok: false }\n const r = await window.craft!.iap.restorePurchases()\n return { ok: !!(r && r.ok) }\n },\n async finishTransaction(transactionId) {\n if (!hasBridge('iap')) return\n await window.craft!.iap.finishTransaction(transactionId)\n },\n async getReceiptData() {\n if (!hasBridge('iap')) return null\n const r = await window.craft!.iap.getReceiptData()\n return r ? String(r) : null\n },\n onPurchased(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:purchased', cb) },\n onFailed(cb) { return onCraftEvent<IAPFailureEvent>('craft:iap:failed', cb) },\n onRestored(cb) { return onCraftEvent<IAPTransactionEvent>('craft:iap:restored', cb) },\n onProductsLoaded(cb) {\n return onCraftEvent<{ products?: IAPProduct[] }>('craft:iap:productsLoaded', (e) => cb(e.products || []))\n },\n onRefunded(cb) { return onCraftEvent<IAPRefundEvent>('craft:iap:refunded', cb) },\n onSubscriptionStatusChanged(cb) {\n return onCraftEvent<IAPSubscriptionStatusEvent>('craft:iap:subscriptionStatusChanged', cb)\n },\n async getActiveSubscriptions() {\n if (!hasBridge('iap')) return []\n // Older bridges may not implement this — defensive default keeps the\n // call shape stable so callers can ship before the native side ships.\n const fn = window.craft!.iap.getActiveSubscriptions\n if (typeof fn !== 'function') return []\n const r = await fn()\n return Array.isArray(r) ? r as IAPSubscriptionStatusEvent[] : []\n },\n async isEligibleForIntroOffer(productId) {\n if (!hasBridge('iap')) return false\n const fn = window.craft!.iap.isEligibleForIntroOffer\n if (typeof fn !== 'function') return false\n return !!(await fn(productId))\n },\n}\n",
44
44
  "/**\n * Handoff (Apple Continuity)\n *\n * `NSUserActivity` lets one of the user's Apple devices pick up a\n * task started on another. A reading app can publish \"currently\n * reading X at chapter Y\" and have the user's iPad pick up exactly\n * where they left off, etc.\n *\n * The bridge exposes:\n *\n * - `startActivity(type, opts)` — broadcast a new activity. The\n * `type` is the activity name\n * declared in `Info.plist`'s\n * `NSUserActivityTypes`.\n * - `updateActivity(opts)` — mutate the in-flight activity.\n * - `stopActivity()` — invalidate.\n * - `getCurrentActivity()` — read the current snapshot.\n * - `onIncoming(cb)` — subscribe to incoming handoffs\n * from another device.\n *\n * **Required Info.plist setup:** add a `NSUserActivityTypes` array\n * listing the activity-type strings your app will use. Without that\n * declaration, macOS rejects the activity at registration time.\n *\n * Browser fallback: this module is a graceful no-op (subscriptions\n * never fire). Handoff has no web equivalent.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport interface HandoffActivityOptions {\n /** Human-readable title shown in the Handoff UI. */\n title?: string\n /** URL to fall back to on devices without the app installed. */\n webpageURL?: string\n /**\n * Arbitrary state to send to the receiving device. Must be plist-\n * compatible (strings, numbers, bools, arrays, dicts of the same).\n * Round-trips through NSJSONSerialization.\n */\n userInfo?: Record<string, unknown>\n}\n\nexport interface HandoffSnapshot {\n type: string\n title: string\n webpageURL: string\n}\n\nexport interface HandoffIncomingEvent {\n type: string\n title?: string\n webpageURL?: string\n userInfo?: Record<string, unknown>\n}\n\nexport interface HandoffAPI {\n /** Start broadcasting a new activity. Resolves to true on success. */\n startActivity: (type: string, options?: HandoffActivityOptions) => Promise<boolean>\n /** Update the in-flight activity (title / webpageURL / userInfo). */\n updateActivity: (options: HandoffActivityOptions) => Promise<boolean>\n /** Invalidate the current activity. Idempotent. */\n stopActivity: () => Promise<void>\n /** Read the current activity, or null if none. */\n getCurrentActivity: () => Promise<HandoffSnapshot | null>\n /** Subscribe to incoming handoffs from another device. */\n onIncoming: (cb: (event: HandoffIncomingEvent) => void) => () => void\n}\n\nexport const handoff: HandoffAPI = {\n async startActivity(type, options) {\n if (!type) throw new Error('handoff.startActivity: type is required')\n if (!hasBridge('handoff')) return false\n // Tolerate both shapes: the production JS facade in craft-bridge.js\n // extracts `{ok:boolean}` to a bare boolean, but the TS-only mock\n // path returns the raw `{ok}` envelope. Either way we surface a\n // single boolean to the caller so call sites stay clean.\n const r = await window.craft!.handoff.startActivity(type, options)\n return typeof r === 'boolean' ? r : !!(r && r.ok)\n },\n async updateActivity(options) {\n if (!hasBridge('handoff')) return false\n const r = await window.craft!.handoff.updateActivity(options)\n return typeof r === 'boolean' ? r : !!(r && r.ok)\n },\n async stopActivity() {\n if (!hasBridge('handoff')) return\n await window.craft!.handoff.stopActivity()\n },\n async getCurrentActivity() {\n if (!hasBridge('handoff')) return null\n const r = await window.craft!.handoff.getCurrentActivity()\n return r && typeof r.type === 'string' ? r : null\n },\n onIncoming(cb) {\n return onCraftEvent<HandoffIncomingEvent>('craft:handoff:incoming', cb)\n },\n}\n",
45
45
  "/**\n * Live Activities — a thin wrapper over Handoff.\n *\n * **Apple's \"Live Activities\" are an iOS-only ActivityKit feature**\n * that lights up Lock Screens and Dynamic Island. They require an\n * iOS 16+ Widget Extension target and can't be invoked from a\n * Craft window directly.\n *\n * What this module ships is the *macOS-compatible approximation*:\n * we publish an NSUserActivity that nearby Apple devices (including\n * the user's iPhone, if it's running a companion app) can pick up\n * via Handoff. It's not a Live Activity in the iOS sense, but it\n * exposes the same conceptual API so app code can be future-ready\n * and dual-targeted.\n *\n * For real iOS Live Activities, ship a Widget Extension target and\n * call `Activity.request(...)` from Swift. This module is the\n * cross-platform glue — same surface, best-effort behaviour.\n */\nimport { handoff } from './handoff'\n\nexport interface LiveActivityState {\n /** Title shown in the Lock Screen / nearby device UI. */\n title?: string\n /** URL to fall back to on devices without the app. */\n webpageURL?: string\n /** Free-form payload passed through to the receiving device. */\n state?: Record<string, unknown>\n}\n\nexport interface LiveActivitiesAPI {\n /**\n * Start a live-activity-shaped session. On macOS this maps to an\n * `NSUserActivity` published via Handoff.\n * @param type — the activity type identifier (must be declared in\n * `Info.plist > NSUserActivityTypes`).\n */\n start: (type: string, state?: LiveActivityState) => Promise<boolean>\n /** Push a new state. Devices receiving the activity see the latest. */\n update: (state: LiveActivityState) => Promise<boolean>\n /** End the activity. Idempotent. */\n stop: () => Promise<void>\n}\n\nexport const liveActivities: LiveActivitiesAPI = {\n async start(type, state) {\n return handoff.startActivity(type, {\n title: state?.title,\n webpageURL: state?.webpageURL,\n userInfo: state?.state,\n })\n },\n async update(state) {\n return handoff.updateActivity({\n title: state.title,\n webpageURL: state.webpageURL,\n userInfo: state.state,\n })\n },\n async stop() {\n await handoff.stopActivity()\n },\n}\n",
46
- "/**\n * Geolocation (CoreLocation on macOS).\n *\n * Apps that need higher-than-browser-grade accuracy use this module —\n * the macOS native path delivers GPS/WiFi-positioning samples directly\n * from `CLLocationManager`. Browser fallback uses the standard\n * `navigator.geolocation` API, which is good enough for \"what city\n * am I in\" but not for navigation-grade tracking.\n *\n * **Required Info.plist keys** for the permission prompt:\n * - `NSLocationWhenInUseUsageDescription` — when in use only\n * - `NSLocationAlwaysAndWhenInUseUsageDescription` — background access\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type LocationAuthStatus = \n| 'undetermined'\n| 'restricted-or-denied'\n| 'authorizedAlways'\n| 'authorizedWhenInUse'\n| 'not-supported'\n| 'unknown'\n\nexport interface LocationCoordinate {\n latitude: number\n longitude: number\n altitude?: number\n /** Horizontal accuracy in meters. Negative = invalid. */\n horizontalAccuracy?: number\n verticalAccuracy?: number\n /** Speed in m/s. Negative = invalid. */\n speed?: number\n}\n\nexport interface LocationWatchOptions {\n /** `'continuous'` (high accuracy, more battery) or `'significant'`. */\n mode?: 'continuous' | 'significant'\n /** Distance in meters between updates. */\n distanceFilter?: number\n}\n\nexport interface LocationAPI {\n /** Trigger the system permission prompt. */\n requestPermission: (mode?: 'whenInUse' | 'always') => Promise<LocationAuthStatus>\n /** Read current authorization status without prompting. */\n getAuthorization: () => Promise<LocationAuthStatus>\n /**\n * Request a single location sample. The result arrives via `onUpdate`,\n * not as the resolution value of this call (CoreLocation is async).\n */\n getCurrentLocation: () => Promise<{ requested: boolean }>\n /** Start streaming updates via `onUpdate`. */\n startWatching: (options?: LocationWatchOptions) => Promise<boolean>\n /** Stop the update stream. */\n stopWatching: () => Promise<void>\n /** Subscribe to location samples. */\n onUpdate: (cb: (loc: LocationCoordinate) => void) => () => void\n /** Subscribe to errors (e.g. denied, location unavailable). */\n onError: (cb: (err: { message: string }) => void) => () => void\n /** Subscribe to authorization-status changes. */\n onAuthChanged: (cb: (info: { status: LocationAuthStatus }) => void) => () => void\n}\n\nexport const location: LocationAPI = {\n async requestPermission(mode = 'whenInUse') {\n if (hasBridge('location')) return await window.craft!.location.requestPermission(mode)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n // Browser geolocation prompts implicitly on the first\n // getCurrentPosition() call — there's no separate request API.\n return 'undetermined'\n }\n return 'not-supported'\n },\n async getAuthorization() {\n if (hasBridge('location')) return await window.craft!.location.getAuthorization()\n return 'unknown'\n },\n async getCurrentLocation() {\n if (hasBridge('location')) return await window.craft!.location.getCurrentLocation()\n // Web fallback: kick off navigator.geolocation.getCurrentPosition,\n // and synthesize a `craft:location:update` event so apps that\n // subscribe via `onUpdate` see the result through the same channel.\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n ;(navigator as any).geolocation.getCurrentPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n (err: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:error', {\n detail: { message: err.message || String(err) },\n }))\n },\n )\n return { requested: true }\n }\n return { requested: false }\n },\n async startWatching(options) {\n if (hasBridge('location')) return await window.craft!.location.startWatching(options)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const watchId = (navigator as any).geolocation.watchPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n )\n ;(window as any).__craftWebLocationWatchId = watchId\n return true\n }\n return false\n },\n async stopWatching() {\n if (hasBridge('location')) {\n await window.craft!.location.stopWatching()\n return\n }\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const id = (window as any).__craftWebLocationWatchId\n if (id != null) {\n ;(navigator as any).geolocation.clearWatch(id)\n ;(window as any).__craftWebLocationWatchId = null\n }\n }\n },\n onUpdate(cb) { return onCraftEvent<LocationCoordinate>('craft:location:update', cb) },\n onError(cb) { return onCraftEvent<{ message: string }>('craft:location:error', cb) },\n onAuthChanged(cb) { return onCraftEvent<{ status: LocationAuthStatus }>('craft:location:authChanged', cb) },\n}\n",
46
+ "/**\n * Geolocation (CoreLocation on macOS).\n *\n * Apps that need higher-than-browser-grade accuracy use this module —\n * the macOS native path delivers GPS/WiFi-positioning samples directly\n * from `CLLocationManager`. Browser fallback uses the standard\n * `navigator.geolocation` API, which is good enough for \"what city\n * am I in\" but not for navigation-grade tracking.\n *\n * **Required Info.plist keys** for the permission prompt:\n * - `NSLocationWhenInUseUsageDescription` — when in use only\n * - `NSLocationAlwaysAndWhenInUseUsageDescription` — background access\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\nexport type LocationAuthStatus =\n| 'undetermined'\n| 'restricted-or-denied'\n| 'authorizedAlways'\n| 'authorizedWhenInUse'\n| 'not-supported'\n| 'unknown'\n\nexport interface LocationCoordinate {\n latitude: number\n longitude: number\n altitude?: number\n /** Horizontal accuracy in meters. Negative = invalid. */\n horizontalAccuracy?: number\n verticalAccuracy?: number\n /** Speed in m/s. Negative = invalid. */\n speed?: number\n}\n\nexport interface LocationWatchOptions {\n /** `'continuous'` (high accuracy, more battery) or `'significant'`. */\n mode?: 'continuous' | 'significant'\n /** Distance in meters between updates. */\n distanceFilter?: number\n}\n\nexport interface LocationAPI {\n /** Trigger the system permission prompt. */\n requestPermission: (mode?: 'whenInUse' | 'always') => Promise<LocationAuthStatus>\n /** Read current authorization status without prompting. */\n getAuthorization: () => Promise<LocationAuthStatus>\n /**\n * Request a single location sample. The result arrives via `onUpdate`,\n * not as the resolution value of this call (CoreLocation is async).\n */\n getCurrentLocation: () => Promise<{ requested: boolean }>\n /** Start streaming updates via `onUpdate`. */\n startWatching: (options?: LocationWatchOptions) => Promise<boolean>\n /** Stop the update stream. */\n stopWatching: () => Promise<void>\n /** Subscribe to location samples. */\n onUpdate: (cb: (loc: LocationCoordinate) => void) => () => void\n /** Subscribe to errors (e.g. denied, location unavailable). */\n onError: (cb: (err: { message: string }) => void) => () => void\n /** Subscribe to authorization-status changes. */\n onAuthChanged: (cb: (info: { status: LocationAuthStatus }) => void) => () => void\n}\n\nexport const location: LocationAPI = {\n async requestPermission(mode = 'whenInUse') {\n if (hasBridge('location')) return await window.craft!.location.requestPermission(mode)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n // Browser geolocation prompts implicitly on the first\n // getCurrentPosition() call — there's no separate request API.\n return 'undetermined'\n }\n return 'not-supported'\n },\n async getAuthorization() {\n if (hasBridge('location')) return await window.craft!.location.getAuthorization()\n return 'unknown'\n },\n async getCurrentLocation() {\n if (hasBridge('location')) return await window.craft!.location.getCurrentLocation()\n // Web fallback: kick off navigator.geolocation.getCurrentPosition,\n // and synthesize a `craft:location:update` event so apps that\n // subscribe via `onUpdate` see the result through the same channel.\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n ;(navigator as any).geolocation.getCurrentPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n (err: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:error', {\n detail: { message: err.message || String(err) },\n }))\n },\n )\n return { requested: true }\n }\n return { requested: false }\n },\n async startWatching(options) {\n if (hasBridge('location')) return await window.craft!.location.startWatching(options)\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const watchId = (navigator as any).geolocation.watchPosition(\n (pos: any) => {\n window.dispatchEvent(new CustomEvent('craft:location:update', {\n detail: {\n latitude: pos.coords.latitude,\n longitude: pos.coords.longitude,\n altitude: pos.coords.altitude,\n horizontalAccuracy: pos.coords.accuracy,\n verticalAccuracy: pos.coords.altitudeAccuracy,\n speed: pos.coords.speed,\n },\n }))\n },\n )\n ;(window as any).__craftWebLocationWatchId = watchId\n return true\n }\n return false\n },\n async stopWatching() {\n if (hasBridge('location')) {\n await window.craft!.location.stopWatching()\n return\n }\n if (typeof navigator !== 'undefined' && (navigator as any).geolocation) {\n const id = (window as any).__craftWebLocationWatchId\n if (id != null) {\n ;(navigator as any).geolocation.clearWatch(id)\n ;(window as any).__craftWebLocationWatchId = null\n }\n }\n },\n onUpdate(cb) { return onCraftEvent<LocationCoordinate>('craft:location:update', cb) },\n onError(cb) { return onCraftEvent<{ message: string }>('craft:location:error', cb) },\n onAuthChanged(cb) { return onCraftEvent<{ status: LocationAuthStatus }>('craft:location:authChanged', cb) },\n}\n",
47
47
  "/**\n * Programmatic screen + window capture.\n *\n * macOS uses `CGWindowListCreateImage` (works on every supported\n * release; permission-gated by Privacy → Screen Recording). Apps\n * should guide the user via `permissions.openSettings('screen_recording')`\n * before calling — otherwise the capture returns a black image.\n *\n * No web fallback — `getDisplayMedia()` exists but it's a different\n * shape (live stream vs still). Calls outside Craft return null.\n */\nimport { hasBridge } from './_bridge'\n\nexport interface CapturableWindow {\n /** CGWindowID — pass to `captureWindow()`. */\n id: number\n /** Window title. May be empty. */\n name: string\n /** Owning app's name (e.g. \"Safari\", \"Finder\"). */\n ownerName: string\n}\n\nexport interface ScreenCaptureAPI {\n /**\n * Capture the entire primary display. Returns a `data:image/png;base64,...`\n * data URL ready to drop into an `<img src>`, or null if the capture\n * failed (typically permission-denied).\n */\n captureScreen: () => Promise<string | null>\n /** Capture a specific window by its CGWindowID. */\n captureWindow: (id: number) => Promise<string | null>\n /** List every on-screen window the OS will let us capture. */\n listWindows: () => Promise<CapturableWindow[]>\n}\n\nexport const screenCapture: ScreenCaptureAPI = {\n async captureScreen() {\n if (!hasBridge('screenCapture')) return null\n const r = await window.craft!.screenCapture.captureScreen()\n return r ? String(r) : null\n },\n async captureWindow(id) {\n if (!hasBridge('screenCapture')) return null\n if (!Number.isFinite(id) || id <= 0) throw new Error('captureWindow: id must be a positive number')\n const r = await window.craft!.screenCapture.captureWindow(id)\n return r ? String(r) : null\n },\n async listWindows() {\n if (!hasBridge('screenCapture')) return []\n return await window.craft!.screenCapture.listWindows()\n },\n}\n",
48
48
  "/**\n * Screen-sharing and screen-recording detection.\n *\n * macOS exposes no direct \"is my screen being captured?\" API, so Craft\n * combines four independent signals: the CGSession dictionary's shared-screen\n * and off-console keys, the floating sharing control conferencing apps show\n * while a share is live, and recorder windows.\n *\n * The window-level signals match the sharing *indicator*, never the presence\n * of the app. \"Zoom is running\" describes most of a working day; acting on it\n * would silence notifications permanently.\n *\n * No web fallback: `getDisplayMedia()` tells a page about its own capture, not\n * about the machine. Outside Craft, `getState()` reports nothing detected and\n * `watch()` resolves without starting anything.\n */\nimport { hasBridge, onCraftEvent } from './_bridge'\n\n/** Which signal produced a detection. */\nexport type ScreenSharingKind = 'system' | 'remote' | 'conference' | 'recording'\n\nexport interface ScreenSharingSource {\n /** Owning application, as the window server reports it. */\n app: string\n /** Window title that matched. Empty when the owner alone was the signal. */\n window: string\n kind: ScreenSharingKind\n}\n\nexport interface ScreenSharingSignals {\n /** macOS Screen Sharing / Apple Remote Desktop has the session. */\n systemScreenShare: boolean\n /** The session is being driven from somewhere other than this console. */\n remoteSession: boolean\n /** A conferencing app is showing its live sharing control. */\n conferenceSharing: boolean\n /** A recorder is capturing the screen. */\n screenRecording: boolean\n}\n\nexport interface ScreenSharingState {\n /** True when any signal fired. */\n sharing: boolean\n signals: ScreenSharingSignals\n /** Every indicator that matched, so apps can explain *why* they reacted. */\n sources: ScreenSharingSource[]\n}\n\n/** Interval bounds enforced natively; mirrored here so callers get the same clamp. */\nexport const MIN_WATCH_INTERVAL_MS = 250\nexport const MAX_WATCH_INTERVAL_MS = 60_000\nexport const DEFAULT_WATCH_INTERVAL_MS = 2000\n\nexport interface ScreenSharingAPI {\n /** One-shot evaluation of every signal. */\n getState: () => Promise<ScreenSharingState>\n /**\n * Start polling. Fires `onChange` once immediately, then only when the\n * resolved state actually differs. Returns the interval the native side\n * settled on after clamping.\n */\n watch: (intervalMs?: number) => Promise<number>\n stop: () => Promise<void>\n /** Subscribe to state changes. Returns an unsubscribe function. */\n onChange: (cb: (state: ScreenSharingState) => void) => () => void\n}\n\nconst IDLE: ScreenSharingState = {\n sharing: false,\n signals: {\n systemScreenShare: false,\n remoteSession: false,\n conferenceSharing: false,\n screenRecording: false,\n },\n sources: [],\n}\n\nfunction idleState(): ScreenSharingState {\n return { ...IDLE, signals: { ...IDLE.signals }, sources: [] }\n}\n\nexport const screenSharing: ScreenSharingAPI = {\n async getState() {\n if (!hasBridge('screenSharing')) return idleState()\n return await window.craft!.screenSharing.getState()\n },\n\n async watch(intervalMs = DEFAULT_WATCH_INTERVAL_MS) {\n const clamped = Math.min(MAX_WATCH_INTERVAL_MS, Math.max(MIN_WATCH_INTERVAL_MS, Math.round(intervalMs)))\n if (!hasBridge('screenSharing')) return clamped\n const r = await window.craft!.screenSharing.watch(clamped)\n return (r && r.intervalMs) || clamped\n },\n\n async stop() {\n if (!hasBridge('screenSharing')) return\n await window.craft!.screenSharing.unwatch()\n },\n\n // Subscribing to the window event directly, rather than through the bridge\n // facade's own `onChange`, keeps this working before the bridge has\n // finished injecting — a listener registered early still receives the first\n // emission that `watch()` sends immediately on start.\n onChange(cb) {\n return onCraftEvent('craft:screenSharing:change', cb)\n },\n}\n\n/**\n * Subscribe and start polling in one call, returning a single teardown.\n *\n * The two halves are easy to leak apart — `onChange` without `watch` never\n * fires, and `watch` without a matching `stop` keeps the timer alive across a\n * page reload.\n */\nexport async function watchScreenSharing(\n cb: (state: ScreenSharingState) => void,\n intervalMs: number = DEFAULT_WATCH_INTERVAL_MS,\n): Promise<() => void> {\n const off = screenSharing.onChange(cb)\n await screenSharing.watch(intervalMs)\n return () => {\n off()\n void screenSharing.stop()\n }\n}\n",
49
49
  "/**\n * Do Not Disturb / Focus.\n *\n * Reading Focus is public macOS API (`INFocusStatusCenter`, macOS 12+) but\n * permission-gated: the app must call `requestAuthorization()` and declare\n * `NSFocusStatusUsageDescription` in its `Info.plist`.\n *\n * Writing Focus is *not* available to third-party apps. The system service\n * rejects every client without an Apple-private entitlement, so the only\n * sanctioned path is to run a user-created Shortcut containing the **Set\n * Focus** action — which is what `setEnabled()` does. Apps are expected to\n * walk the user through creating those shortcuts once and to verify them with\n * `listShortcuts()` before offering the feature.\n *\n * No web fallback exists: a browser cannot read or set the system's Focus.\n * Outside Craft every call resolves to an unsupported result rather than\n * throwing, so cross-platform code can call unconditionally.\n */\nimport { hasBridge } from './_bridge'\n\n/**\n * Mirrors `INFocusStatusAuthorizationStatus`. `unsupported` is Craft's own\n * value for platforms where the framework isn't present at all.\n */\nexport type FocusAuthorization = 'notDetermined' | 'restricted' | 'denied' | 'authorized' | 'unsupported'\n\n/**\n * How the shortcut is run.\n *\n * `cli` execs `/usr/bin/shortcuts` and reports the shortcut's real exit\n * status. `url` opens `shortcuts://run-shortcut`, the only route the App\n * Sandbox permits — but it is fire-and-forget: LaunchServices confirms it\n * handed the URL over, never that the shortcut ran. `auto` picks `url` under\n * sandbox and `cli` everywhere else, so a real status is used where one\n * exists.\n */\nexport type FocusStrategy = 'auto' | 'cli' | 'url'\n\nexport interface FocusStatus {\n /** False when Focus isn't available on this platform. */\n supported: boolean\n /**\n * Whether the user is in *any* Focus. `null` means the system declined to\n * answer — almost always missing authorization, which is not the same as\n * \"not focused\". Branch on the two separately.\n */\n isFocused: boolean | null\n authorization: FocusAuthorization\n}\n\nexport interface FocusShortcutOptions {\n /** Shortcut to run when turning Focus on. */\n onShortcut?: string\n /** Shortcut to run when turning Focus off. */\n offShortcut?: string\n /** Defaults to `auto`. */\n strategy?: FocusStrategy\n}\n\nexport interface FocusResult {\n ok: boolean\n strategy?: 'shortcut' | 'url'\n /** Exit status of the Shortcuts CLI. Absent for the `url` strategy. */\n exitCode?: number\n /**\n * `url` strategy only: the request reached Shortcuts. Not a claim that the\n * shortcut ran — that signal does not exist on this route.\n */\n dispatched?: boolean\n shortcut?: string\n error?: string\n}\n\nexport interface FocusShortcutList {\n /**\n * False when enumeration was not possible — inside the App Sandbox, or off\n * platform. An empty `shortcuts` then means *could not check*, not *none\n * installed*, and must not send the user through setup again.\n */\n canList: boolean\n shortcuts: string[]\n}\n\nexport interface FocusAPI {\n /** Current Focus state. Never throws — check `supported` first. */\n getStatus: () => Promise<FocusStatus>\n /** Present the system permission prompt for Focus status. */\n requestAuthorization: () => Promise<FocusAuthorization>\n /**\n * Turn Focus on or off by running the matching user shortcut. Resolves with\n * `ok: false` and a reason rather than throwing, because the common failure\n * — the shortcut doesn't exist yet — is something the app should surface to\n * the user, not treat as a crash.\n */\n setEnabled: (enabled: boolean, options?: FocusShortcutOptions) => Promise<FocusResult>\n /** Run any shortcut by name — for per-mode or timed Focus flows. */\n runShortcut: (name: string) => Promise<FocusResult>\n /** Every shortcut installed for the current user. Empty off-platform. */\n listShortcuts: () => Promise<string[]>\n /** Same, plus whether enumeration was possible at all. */\n listShortcutsResult: () => Promise<FocusShortcutList>\n}\n\nconst UNSUPPORTED: FocusStatus = { supported: false, isFocused: null, authorization: 'unsupported' }\n\nfunction unavailable(): FocusResult {\n return { ok: false, error: 'Focus control is only available in a Craft window on macOS' }\n}\n\nexport const focus: FocusAPI = {\n async getStatus() {\n if (!hasBridge('focus')) return { ...UNSUPPORTED }\n return await window.craft!.focus.getStatus()\n },\n\n async requestAuthorization() {\n if (!hasBridge('focus')) return 'unsupported'\n return await window.craft!.focus.requestAuthorization()\n },\n\n async setEnabled(enabled, options = {}) {\n if (!hasBridge('focus')) return unavailable()\n const name = enabled ? options.onShortcut : options.offShortcut\n if (!name) {\n return {\n ok: false,\n error: `focus.setEnabled: no ${enabled ? 'onShortcut' : 'offShortcut'} configured`,\n }\n }\n return await window.craft!.focus.setEnabled(enabled, options)\n },\n\n async runShortcut(name) {\n if (!hasBridge('focus')) return unavailable()\n if (!name) throw new Error('focus.runShortcut: name is required')\n return await window.craft!.focus.runShortcut(name)\n },\n\n async listShortcuts() {\n if (!hasBridge('focus')) return []\n return await window.craft!.focus.listShortcuts()\n },\n\n async listShortcutsResult() {\n if (!hasBridge('focus')) return { canList: false, shortcuts: [] }\n const r = await window.craft!.focus.listShortcutsResult()\n return { canList: Boolean(r?.canList), shortcuts: (r?.shortcuts as string[]) || [] }\n },\n}\n\n/**\n * Whether every shortcut the app depends on is installed.\n *\n * Worth calling on launch: the shortcuts are user-created, so the honest time\n * to discover they're missing is before the user is relying on the feature,\n * not at the moment a meeting starts.\n */\nexport async function hasFocusShortcuts(...names: string[]): Promise<boolean> {\n if (names.length === 0) return false\n const installed = new Set(await focus.listShortcuts())\n return names.every(name => installed.has(name))\n}\n\n/**\n * Like `hasFocusShortcuts`, but distinguishes \"not installed\" from \"could not\n * check\". Prefer this anywhere the answer drives a setup prompt.\n */\nexport async function focusShortcutsReady(...names: string[]): Promise<boolean | 'unknown'> {\n if (names.length === 0) return false\n const { canList, shortcuts } = await focus.listShortcutsResult()\n if (!canList) return 'unknown'\n const installed = new Set(shortcuts)\n return names.every(name => installed.has(name))\n}\n",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/desktop",
3
3
  "type": "module",
4
- "version": "0.2.175",
4
+ "version": "0.2.177",
5
5
  "description": "Native desktop application framework for stx (powered by Craft)",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",