@elizaos/capacitor-network-policy 2.0.11-beta.7

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.
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ElizaosCapacitorNetworkPolicy'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license'] || { :type => 'MIT' }
10
+ s.homepage = 'https://elizaos.ai'
11
+ s.authors = { 'elizaOS' => 'dev@elizaos.ai' }
12
+ s.source = { :git => 'https://github.com/elizaOS/eliza.git', :tag => s.version.to_s }
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.ios.deployment_target = '13.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.1'
17
+ end
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shaw Walters and elizaOS Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @elizaos/capacitor-network-policy
2
+
3
+ Android + iOS Capacitor plugin that surfaces OS-level network-metering hints to Eliza agents running on mobile devices.
4
+
5
+ ## What it does
6
+
7
+ The voice-model auto-updater in `plugin-local-inference` must decide whether to download updated model weights over the current network link. This plugin bridges two OS APIs:
8
+
9
+ - **Android** — `ConnectivityManager.getNetworkCapabilities(activeNetwork).hasCapability(NET_CAPABILITY_NOT_METERED)`. Returns `metered: true | false | null` (`null` = unknown/no-permission).
10
+ - **iOS** — `NWPathMonitor.currentPath.isExpensive` and `.isConstrained`. `isExpensive` is Apple's canonical "treat as metered" flag (covers cellular and tethered hotspot Wi-Fi). `isConstrained` is Low Data Mode.
11
+ - **Web / browser fallback** — reads `navigator.connection.saveData`; returns `null` when the signal is absent so the policy falls through to "ask the user."
12
+
13
+ On import, the plugin installs `globalThis.ElizaNetworkPolicy` so `plugin-local-inference/src/services/network-policy.ts` can call into the bridge without a compile-time Capacitor dependency.
14
+
15
+ ## Capacitor bridge
16
+
17
+ Bridge name: `ElizaNetworkPolicy`
18
+
19
+ ### `getMeteredHint(): Promise<MeteredHint>`
20
+
21
+ **Android (safe fallback on iOS/web).** Returns `{ metered: boolean | null, source: "android-os" }`.
22
+
23
+ | `metered` value | Meaning |
24
+ |-----------------|---------|
25
+ | `true` | Link is metered — defer or skip the download. |
26
+ | `false` | Link is not metered — download is safe. |
27
+ | `null` | No active network, permission denied, or OS cannot report. Policy falls through to "ask." |
28
+
29
+ > Android explicitly warns that transport type (cellular vs. Wi-Fi) is not a reliable proxy for metering. Use only the `NET_CAPABILITY_NOT_METERED` flag.
30
+
31
+ ### `getPathHints(): Promise<PathHints>`
32
+
33
+ **iOS (safe fallback on Android/web).** Returns `{ isExpensive: boolean, isConstrained: boolean, source: "nw-path-monitor" }`.
34
+
35
+ | Field | Meaning |
36
+ |-------|---------|
37
+ | `isExpensive` | `true` when the link is cellular or a cellular hotspot — treat as metered. |
38
+ | `isConstrained` | `true` when Low Data Mode is on — user has explicitly asked the OS to limit non-essential traffic. |
39
+
40
+ Returns `false/false` on Android (use `getMeteredHint()` there).
41
+
42
+ ## Requirements
43
+
44
+ - `@capacitor/core ^8.3.1` (peer dependency — provided by the consuming app)
45
+ - Android: `ACCESS_NETWORK_STATE` permission in `AndroidManifest.xml`
46
+ - iOS 13+, Swift 5.1+
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ bun add @elizaos/capacitor-network-policy
52
+ npx cap sync
53
+ ```
54
+
55
+ For iOS, add the pod to your Podfile:
56
+
57
+ ```ruby
58
+ pod 'ElizaosCapacitorNetworkPolicy', :path => '../node_modules/@elizaos/capacitor-network-policy'
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ```ts
64
+ import "@elizaos/capacitor-network-policy";
65
+ // globalThis.ElizaNetworkPolicy is now set (side-effect on import).
66
+
67
+ import { NetworkPolicy } from "@elizaos/capacitor-network-policy";
68
+
69
+ // Android
70
+ const { metered } = await NetworkPolicy.getMeteredHint();
71
+ if (metered === true) { /* skip download */ }
72
+
73
+ // iOS
74
+ const { isExpensive, isConstrained } = await NetworkPolicy.getPathHints();
75
+ if (isExpensive || isConstrained) { /* skip download */ }
76
+ ```
77
+
78
+ ## Build
79
+
80
+ ```bash
81
+ bun run --cwd plugins/plugin-native-network-policy build
82
+ ```
83
+
84
+ Outputs:
85
+ - `dist/esm/index.js` — ESM (TypeScript-compiled)
86
+ - `dist/plugin.js` — IIFE for web bundlers
87
+ - `dist/plugin.cjs.js` — CommonJS
@@ -0,0 +1,27 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ }
4
+
5
+ apply plugin: 'com.android.library'
6
+ android {
7
+ namespace = "ai.eliza.plugins.networkpolicy"
8
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
9
+ defaultConfig {
10
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
11
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
12
+ }
13
+ compileOptions {
14
+ sourceCompatibility JavaVersion.VERSION_17
15
+ targetCompatibility JavaVersion.VERSION_17
16
+ }
17
+ }
18
+
19
+ repositories {
20
+ google()
21
+ maven { url = uri(rootProject.ext.has('mavenCentralMirrorUrl') ? rootProject.ext.mavenCentralMirrorUrl : 'https://repo.maven.apache.org/maven2') }
22
+ mavenCentral()
23
+ }
24
+
25
+ dependencies {
26
+ implementation project(':capacitor-android')
27
+ }
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <!--
4
+ ACCESS_NETWORK_STATE is required to call
5
+ `ConnectivityManager.getNetworkCapabilities`. The plugin gracefully
6
+ returns `metered: null` (TS downgrades to ask) when the permission
7
+ is denied so callers don't have to handle a SecurityException.
8
+ -->
9
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
10
+ </manifest>
@@ -0,0 +1,95 @@
1
+ package ai.eliza.plugins.networkpolicy
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.net.NetworkCapabilities
6
+ import com.getcapacitor.JSObject
7
+ import com.getcapacitor.Plugin
8
+ import com.getcapacitor.PluginCall
9
+ import com.getcapacitor.PluginMethod
10
+ import com.getcapacitor.annotation.CapacitorPlugin
11
+
12
+ /**
13
+ * Android `metered` hint bridge for the voice-model auto-updater
14
+ * (R5-versioning §4.1).
15
+ *
16
+ * Reads `ConnectivityManager.getNetworkCapabilities(activeNetwork)
17
+ * .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)`.
18
+ *
19
+ * Android explicitly warns: "Do not assume that cellular means metered.
20
+ * On many devices a tethered Wi-Fi hotspot reports `wifi` as the transport
21
+ * but is metered, and on others a corporate cellular plan reports as
22
+ * not-metered." The metered flag is the only authoritative source.
23
+ *
24
+ * Returned shape (mirrors the TS `MeteredHint` interface):
25
+ *
26
+ * { metered: true | false | null, source: "android-os" }
27
+ *
28
+ * `null` is returned when there is no active network, when the
29
+ * `NetworkCapabilities` object is unavailable (lock-screen / boot races),
30
+ * or when the system lacks `ACCESS_NETWORK_STATE` permission — the TS
31
+ * side then downgrades to `unknown → ask`.
32
+ */
33
+ @CapacitorPlugin(name = "ElizaNetworkPolicy")
34
+ class NetworkPolicyPlugin : Plugin() {
35
+ private val connectivityManager: ConnectivityManager?
36
+ get() = context.applicationContext
37
+ .getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
38
+
39
+ @PluginMethod
40
+ fun getMeteredHint(call: PluginCall) {
41
+ val response = JSObject()
42
+ response.put("source", "android-os")
43
+ val cm = connectivityManager
44
+ if (cm == null) {
45
+ response.put("metered", JSObject.NULL)
46
+ call.resolve(response)
47
+ return
48
+ }
49
+ val active = try {
50
+ cm.activeNetwork
51
+ } catch (t: Throwable) {
52
+ null
53
+ }
54
+ if (active == null) {
55
+ response.put("metered", JSObject.NULL)
56
+ call.resolve(response)
57
+ return
58
+ }
59
+ val caps: NetworkCapabilities? = try {
60
+ cm.getNetworkCapabilities(active)
61
+ } catch (t: SecurityException) {
62
+ // Missing ACCESS_NETWORK_STATE. Surface as unknown rather than
63
+ // throw — the caller will downgrade to `ask`.
64
+ null
65
+ } catch (t: Throwable) {
66
+ null
67
+ }
68
+ if (caps == null) {
69
+ response.put("metered", JSObject.NULL)
70
+ call.resolve(response)
71
+ return
72
+ }
73
+ // `hasCapability(NET_CAPABILITY_NOT_METERED)` is true when the link
74
+ // is NOT metered. We surface the inverse (whether it IS metered)
75
+ // because that's the field the TS decision rule consumes.
76
+ val notMetered = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
77
+ response.put("metered", !notMetered)
78
+ call.resolve(response)
79
+ }
80
+
81
+ /**
82
+ * iOS-only safe fallback on Android. Always resolves with the shared
83
+ * response shape so the TS bridge can call `getPathHints()` uniformly;
84
+ * the TS layer treats a never-expensive / never-constrained Android
85
+ * response as "no info" and falls back to `getMeteredHint`.
86
+ */
87
+ @PluginMethod
88
+ fun getPathHints(call: PluginCall) {
89
+ val response = JSObject()
90
+ response.put("isExpensive", false)
91
+ response.put("isConstrained", false)
92
+ response.put("source", "nw-path-monitor")
93
+ call.resolve(response)
94
+ }
95
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Type definitions for the `@elizaos/capacitor-network-policy` bridge.
3
+ *
4
+ * Backs the on-device portion of the voice-model auto-updater
5
+ * network-policy decision (R5-versioning §4). Two methods:
6
+ *
7
+ * - `getMeteredHint()` — Android. Wraps
8
+ * `ConnectivityManager.getNetworkCapabilities(activeNetwork).hasCapability(
9
+ * NetworkCapabilities.NET_CAPABILITY_NOT_METERED)`. Android docs warn
10
+ * explicitly that "cellular" is NOT a synonym for "metered" so the
11
+ * metered flag is mandatory.
12
+ * - `getPathHints()` — iOS. Wraps `NWPathMonitor.currentPath.isExpensive`
13
+ * and `.isConstrained`. `isExpensive == true` is Apple's "treat as
14
+ * metered" flag (cellular by default, plus tethered Wi-Fi from a
15
+ * cellular hotspot).
16
+ *
17
+ * The plugin populates `globalThis.ElizaNetworkPolicy` so the platform-
18
+ * agnostic probes in `plugin-local-inference/src/services/network-policy.ts`
19
+ * can read it without depending on Capacitor at compile time.
20
+ */
21
+ export interface MeteredHint {
22
+ /**
23
+ * `true` if Android reports `NET_CAPABILITY_NOT_METERED === false`
24
+ * (i.e. the link IS metered), `false` if not metered, `null` when the
25
+ * platform cannot report a definitive answer (no active network or
26
+ * permission denied).
27
+ */
28
+ metered: boolean | null;
29
+ /** Source label for debugging — always `"android-os"` from this plugin. */
30
+ source: "android-os";
31
+ }
32
+ export interface PathHints {
33
+ /** `NWPath.isExpensive` — true when the link is metered per Apple's policy. */
34
+ isExpensive: boolean;
35
+ /**
36
+ * `NWPath.isConstrained` — true when Low Data Mode is engaged. The voice-
37
+ * updater treats this as "metered" because the user has explicitly asked
38
+ * the OS to limit non-essential traffic.
39
+ */
40
+ isConstrained: boolean;
41
+ /** Source label for debugging — always `"nw-path-monitor"` from this plugin. */
42
+ source: "nw-path-monitor";
43
+ }
44
+ export interface NetworkPolicyPlugin {
45
+ /** Android-only. Returns `{ metered: null, source: "android-os" }` on iOS / web. */
46
+ getMeteredHint(): Promise<MeteredHint>;
47
+ /** iOS-only. Returns `{ isExpensive: false, isConstrained: false, source: "nw-path-monitor" }` on Android / web. */
48
+ getPathHints(): Promise<PathHints>;
49
+ }
50
+ //# sourceMappingURL=definitions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,2EAA2E;IAC3E,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,WAAW,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,gFAAgF;IAChF,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,oFAAoF;IACpF,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACvC,oHAAoH;IACpH,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;CACpC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Type definitions for the `@elizaos/capacitor-network-policy` bridge.
3
+ *
4
+ * Backs the on-device portion of the voice-model auto-updater
5
+ * network-policy decision (R5-versioning §4). Two methods:
6
+ *
7
+ * - `getMeteredHint()` — Android. Wraps
8
+ * `ConnectivityManager.getNetworkCapabilities(activeNetwork).hasCapability(
9
+ * NetworkCapabilities.NET_CAPABILITY_NOT_METERED)`. Android docs warn
10
+ * explicitly that "cellular" is NOT a synonym for "metered" so the
11
+ * metered flag is mandatory.
12
+ * - `getPathHints()` — iOS. Wraps `NWPathMonitor.currentPath.isExpensive`
13
+ * and `.isConstrained`. `isExpensive == true` is Apple's "treat as
14
+ * metered" flag (cellular by default, plus tethered Wi-Fi from a
15
+ * cellular hotspot).
16
+ *
17
+ * The plugin populates `globalThis.ElizaNetworkPolicy` so the platform-
18
+ * agnostic probes in `plugin-local-inference/src/services/network-policy.ts`
19
+ * can read it without depending on Capacitor at compile time.
20
+ */
21
+ export {};
22
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `@elizaos/capacitor-network-policy` — Android `metered` / iOS `isExpensive`
3
+ * shims for the voice-model auto-updater network policy (R5-versioning §4).
4
+ *
5
+ * Registers as `ElizaNetworkPolicy` on the Capacitor bridge. The
6
+ * platform-agnostic probes in `plugin-local-inference` read the bridge
7
+ * via `globalThis.ElizaNetworkPolicy` so this module's only job is to
8
+ * register and install the global.
9
+ */
10
+ import type { NetworkPolicyPlugin } from "./definitions";
11
+ export * from "./definitions";
12
+ export declare const NetworkPolicy: NetworkPolicyPlugin;
13
+ /**
14
+ * Install `globalThis.ElizaNetworkPolicy` so the runtime probe in
15
+ * `plugin-local-inference/src/services/network-policy.ts` can call into
16
+ * the native bridge without compile-time Capacitor dependencies.
17
+ *
18
+ * Idempotent — re-installing replaces the existing handle with the same
19
+ * `NetworkPolicy` instance.
20
+ */
21
+ export declare function installNetworkPolicyGlobal(): void;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,cAAc,eAAe,CAAC;AAI9B,eAAO,MAAM,aAAa,qBAGzB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAIjD"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `@elizaos/capacitor-network-policy` — Android `metered` / iOS `isExpensive`
3
+ * shims for the voice-model auto-updater network policy (R5-versioning §4).
4
+ *
5
+ * Registers as `ElizaNetworkPolicy` on the Capacitor bridge. The
6
+ * platform-agnostic probes in `plugin-local-inference` read the bridge
7
+ * via `globalThis.ElizaNetworkPolicy` so this module's only job is to
8
+ * register and install the global.
9
+ */
10
+ import { registerPlugin } from "@capacitor/core";
11
+ export * from "./definitions";
12
+ const loadWeb = () => import("./web").then((m) => new m.NetworkPolicyWeb());
13
+ export const NetworkPolicy = registerPlugin("ElizaNetworkPolicy", { web: loadWeb });
14
+ /**
15
+ * Install `globalThis.ElizaNetworkPolicy` so the runtime probe in
16
+ * `plugin-local-inference/src/services/network-policy.ts` can call into
17
+ * the native bridge without compile-time Capacitor dependencies.
18
+ *
19
+ * Idempotent — re-installing replaces the existing handle with the same
20
+ * `NetworkPolicy` instance.
21
+ */
22
+ export function installNetworkPolicyGlobal() {
23
+ globalThis.ElizaNetworkPolicy = NetworkPolicy;
24
+ }
25
+ // Side-effect: install on import so callers that simply
26
+ // `import "@elizaos/capacitor-network-policy"` from the app bootstrap
27
+ // pick up the global without any extra wiring.
28
+ installNetworkPolicyGlobal();
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,cAAc,eAAe,CAAC;AAE9B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC;AAE5E,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,CACzC,oBAAoB,EACpB,EAAE,GAAG,EAAE,OAAO,EAAE,CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,0BAA0B;IAEtC,UACD,CAAC,kBAAkB,GAAG,aAAa,CAAC;AACvC,CAAC;AAED,wDAAwD;AACxD,sEAAsE;AACtE,+CAA+C;AAC/C,0BAA0B,EAAE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Web fallback for `@elizaos/capacitor-network-policy`.
3
+ *
4
+ * Browsers can read `navigator.connection.saveData` (a heuristic for "user
5
+ * has Data Saver enabled") and `.type` (`cellular` / `wifi` / `ethernet`),
6
+ * but the WICG spec is partial and Firefox/Safari support is limited. The
7
+ * fallback returns conservative defaults so the policy decision falls
8
+ * through to `unknown → ask` — never silently accepts a download on a
9
+ * potentially-metered link.
10
+ */
11
+ import { WebPlugin } from "@capacitor/core";
12
+ import type { MeteredHint, NetworkPolicyPlugin, PathHints } from "./definitions";
13
+ export declare class NetworkPolicyWeb extends WebPlugin implements NetworkPolicyPlugin {
14
+ getMeteredHint(): Promise<MeteredHint>;
15
+ getPathHints(): Promise<PathHints>;
16
+ }
17
+ //# sourceMappingURL=web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,SAAS,EACV,MAAM,eAAe,CAAC;AAEvB,qBAAa,gBAAiB,SAAQ,SAAU,YAAW,mBAAmB;IACtE,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IAWtC,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;CAQzC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Web fallback for `@elizaos/capacitor-network-policy`.
3
+ *
4
+ * Browsers can read `navigator.connection.saveData` (a heuristic for "user
5
+ * has Data Saver enabled") and `.type` (`cellular` / `wifi` / `ethernet`),
6
+ * but the WICG spec is partial and Firefox/Safari support is limited. The
7
+ * fallback returns conservative defaults so the policy decision falls
8
+ * through to `unknown → ask` — never silently accepts a download on a
9
+ * potentially-metered link.
10
+ */
11
+ import { WebPlugin } from "@capacitor/core";
12
+ export class NetworkPolicyWeb extends WebPlugin {
13
+ async getMeteredHint() {
14
+ const saveData = readNavigatorSaveData();
15
+ // `saveData=true` is a strong signal the user wants metered-mode behavior.
16
+ // We don't infer "metered=false" from `saveData=false` — that just means
17
+ // Data Saver isn't on, which says nothing about metering.
18
+ return {
19
+ metered: saveData === true ? true : null,
20
+ source: "android-os",
21
+ };
22
+ }
23
+ async getPathHints() {
24
+ const saveData = readNavigatorSaveData();
25
+ return {
26
+ isExpensive: saveData === true,
27
+ isConstrained: saveData === true,
28
+ source: "nw-path-monitor",
29
+ };
30
+ }
31
+ }
32
+ function readNavigatorSaveData() {
33
+ try {
34
+ const nav = globalThis.navigator;
35
+ const saveData = nav?.connection?.saveData;
36
+ return typeof saveData === "boolean" ? saveData : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAQ5C,MAAM,OAAO,gBAAiB,SAAQ,SAAS;IAC7C,KAAK,CAAC,cAAc;QAClB,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAC;QACzC,2EAA2E;QAC3E,yEAAyE;QACzE,0DAA0D;QAC1D,OAAO;YACL,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;YACxC,MAAM,EAAE,YAAY;SACrB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAC;QACzC,OAAO;YACL,WAAW,EAAE,QAAQ,KAAK,IAAI;YAC9B,aAAa,EAAE,QAAQ,KAAK,IAAI;YAChC,MAAM,EAAE,iBAAiB;SAC1B,CAAC;IACJ,CAAC;CACF;AAED,SAAS,qBAAqB;IAC5B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,UAAU,CAAC,SAEV,CAAC;QACd,MAAM,QAAQ,GAAG,GAAG,EAAE,UAAU,EAAE,QAAQ,CAAC;QAC3C,OAAO,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=web.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.test.d.ts","sourceRoot":"","sources":["../../src/web.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { NetworkPolicyWeb } from "./web";
3
+ function setNavigator(value) {
4
+ Object.defineProperty(globalThis, "navigator", {
5
+ configurable: true,
6
+ value,
7
+ });
8
+ }
9
+ describe("NetworkPolicyWeb fallback", () => {
10
+ afterEach(() => {
11
+ Object.defineProperty(globalThis, "navigator", {
12
+ configurable: true,
13
+ value: undefined,
14
+ });
15
+ });
16
+ it("treats browser saveData=true as metered and constrained", async () => {
17
+ setNavigator({ connection: { saveData: true } });
18
+ const policy = new NetworkPolicyWeb();
19
+ await expect(policy.getMeteredHint()).resolves.toEqual({
20
+ metered: true,
21
+ source: "android-os",
22
+ });
23
+ await expect(policy.getPathHints()).resolves.toEqual({
24
+ isExpensive: true,
25
+ isConstrained: true,
26
+ source: "nw-path-monitor",
27
+ });
28
+ });
29
+ it.each([
30
+ undefined,
31
+ {},
32
+ { connection: null },
33
+ { connection: { saveData: false } },
34
+ { connection: { saveData: "true" } },
35
+ { connection: { saveData: 1 } },
36
+ ])("falls back conservatively for navigator shape %#", async (navigatorLike) => {
37
+ setNavigator(navigatorLike);
38
+ const policy = new NetworkPolicyWeb();
39
+ await expect(policy.getMeteredHint()).resolves.toEqual({
40
+ metered: null,
41
+ source: "android-os",
42
+ });
43
+ await expect(policy.getPathHints()).resolves.toEqual({
44
+ isExpensive: false,
45
+ isConstrained: false,
46
+ source: "nw-path-monitor",
47
+ });
48
+ });
49
+ });
50
+ //# sourceMappingURL=web.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.test.js","sourceRoot":"","sources":["../../src/web.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAEzC,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,WAAW,EAAE;QAC7C,YAAY,EAAE,IAAI;QAClB,KAAK;KACN,CAAC,CAAC;AACL,CAAC;AAED,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,WAAW,EAAE;YAC7C,YAAY,EAAE,IAAI;YAClB,KAAK,EAAE,SAAS;SACjB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,KAAK,IAAI,EAAE;QACvE,YAAY,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;QAEtC,MAAM,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YACrD,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,YAAY;SACrB,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YACnD,WAAW,EAAE,IAAI;YACjB,aAAa,EAAE,IAAI;YACnB,MAAM,EAAE,iBAAiB;SAC1B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC;QACN,SAAS;QACT,EAAE;QACF,EAAE,UAAU,EAAE,IAAI,EAAE;QACpB,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACnC,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QACpC,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;KAChC,CAAC,CAAC,kDAAkD,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE;QAC7E,YAAY,CAAC,aAAa,CAAC,CAAC;QAC5B,MAAM,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;QAEtC,MAAM,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YACrD,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,YAAY;SACrB,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YACnD,WAAW,EAAE,KAAK;YAClB,aAAa,EAAE,KAAK;YACpB,MAAM,EAAE,iBAAiB;SAC1B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ var core = require('@capacitor/core');
4
+
5
+ /**
6
+ * `@elizaos/capacitor-network-policy` — Android `metered` / iOS `isExpensive`
7
+ * shims for the voice-model auto-updater network policy (R5-versioning §4).
8
+ *
9
+ * Registers as `ElizaNetworkPolicy` on the Capacitor bridge. The
10
+ * platform-agnostic probes in `plugin-local-inference` read the bridge
11
+ * via `globalThis.ElizaNetworkPolicy` so this module's only job is to
12
+ * register and install the global.
13
+ */
14
+ const loadWeb = () => Promise.resolve().then(function () { return web; }).then((m) => new m.NetworkPolicyWeb());
15
+ const NetworkPolicy = core.registerPlugin("ElizaNetworkPolicy", { web: loadWeb });
16
+ /**
17
+ * Install `globalThis.ElizaNetworkPolicy` so the runtime probe in
18
+ * `plugin-local-inference/src/services/network-policy.ts` can call into
19
+ * the native bridge without compile-time Capacitor dependencies.
20
+ *
21
+ * Idempotent — re-installing replaces the existing handle with the same
22
+ * `NetworkPolicy` instance.
23
+ */
24
+ function installNetworkPolicyGlobal() {
25
+ globalThis.ElizaNetworkPolicy = NetworkPolicy;
26
+ }
27
+ // Side-effect: install on import so callers that simply
28
+ // `import "@elizaos/capacitor-network-policy"` from the app bootstrap
29
+ // pick up the global without any extra wiring.
30
+ installNetworkPolicyGlobal();
31
+
32
+ /**
33
+ * Web fallback for `@elizaos/capacitor-network-policy`.
34
+ *
35
+ * Browsers can read `navigator.connection.saveData` (a heuristic for "user
36
+ * has Data Saver enabled") and `.type` (`cellular` / `wifi` / `ethernet`),
37
+ * but the WICG spec is partial and Firefox/Safari support is limited. The
38
+ * fallback returns conservative defaults so the policy decision falls
39
+ * through to `unknown → ask` — never silently accepts a download on a
40
+ * potentially-metered link.
41
+ */
42
+ class NetworkPolicyWeb extends core.WebPlugin {
43
+ async getMeteredHint() {
44
+ const saveData = readNavigatorSaveData();
45
+ // `saveData=true` is a strong signal the user wants metered-mode behavior.
46
+ // We don't infer "metered=false" from `saveData=false` — that just means
47
+ // Data Saver isn't on, which says nothing about metering.
48
+ return {
49
+ metered: saveData === true ? true : null,
50
+ source: "android-os",
51
+ };
52
+ }
53
+ async getPathHints() {
54
+ const saveData = readNavigatorSaveData();
55
+ return {
56
+ isExpensive: saveData === true,
57
+ isConstrained: saveData === true,
58
+ source: "nw-path-monitor",
59
+ };
60
+ }
61
+ }
62
+ function readNavigatorSaveData() {
63
+ try {
64
+ const nav = globalThis.navigator;
65
+ const saveData = nav?.connection?.saveData;
66
+ return typeof saveData === "boolean" ? saveData : null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ var web = /*#__PURE__*/Object.freeze({
74
+ __proto__: null,
75
+ NetworkPolicyWeb: NetworkPolicyWeb
76
+ });
77
+
78
+ exports.NetworkPolicy = NetworkPolicy;
79
+ exports.installNetworkPolicyGlobal = installNetworkPolicyGlobal;
80
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["/**\n * `@elizaos/capacitor-network-policy` — Android `metered` / iOS `isExpensive`\n * shims for the voice-model auto-updater network policy (R5-versioning §4).\n *\n * Registers as `ElizaNetworkPolicy` on the Capacitor bridge. The\n * platform-agnostic probes in `plugin-local-inference` read the bridge\n * via `globalThis.ElizaNetworkPolicy` so this module's only job is to\n * register and install the global.\n */\nimport { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.NetworkPolicyWeb());\nexport const NetworkPolicy = registerPlugin(\"ElizaNetworkPolicy\", { web: loadWeb });\n/**\n * Install `globalThis.ElizaNetworkPolicy` so the runtime probe in\n * `plugin-local-inference/src/services/network-policy.ts` can call into\n * the native bridge without compile-time Capacitor dependencies.\n *\n * Idempotent — re-installing replaces the existing handle with the same\n * `NetworkPolicy` instance.\n */\nexport function installNetworkPolicyGlobal() {\n globalThis.ElizaNetworkPolicy = NetworkPolicy;\n}\n// Side-effect: install on import so callers that simply\n// `import \"@elizaos/capacitor-network-policy\"` from the app bootstrap\n// pick up the global without any extra wiring.\ninstallNetworkPolicyGlobal();\n//# sourceMappingURL=index.js.map","/**\n * Web fallback for `@elizaos/capacitor-network-policy`.\n *\n * Browsers can read `navigator.connection.saveData` (a heuristic for \"user\n * has Data Saver enabled\") and `.type` (`cellular` / `wifi` / `ethernet`),\n * but the WICG spec is partial and Firefox/Safari support is limited. The\n * fallback returns conservative defaults so the policy decision falls\n * through to `unknown → ask` — never silently accepts a download on a\n * potentially-metered link.\n */\nimport { WebPlugin } from \"@capacitor/core\";\nexport class NetworkPolicyWeb extends WebPlugin {\n async getMeteredHint() {\n const saveData = readNavigatorSaveData();\n // `saveData=true` is a strong signal the user wants metered-mode behavior.\n // We don't infer \"metered=false\" from `saveData=false` — that just means\n // Data Saver isn't on, which says nothing about metering.\n return {\n metered: saveData === true ? true : null,\n source: \"android-os\",\n };\n }\n async getPathHints() {\n const saveData = readNavigatorSaveData();\n return {\n isExpensive: saveData === true,\n isConstrained: saveData === true,\n source: \"nw-path-monitor\",\n };\n }\n}\nfunction readNavigatorSaveData() {\n try {\n const nav = globalThis.navigator;\n const saveData = nav?.connection?.saveData;\n return typeof saveData === \"boolean\" ? saveData : null;\n }\n catch {\n return null;\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA,MAAM,OAAO,GAAG,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;AAC/D,MAAC,aAAa,GAAGA,mBAAc,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;AAClF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,0BAA0B,GAAG;AAC7C,IAAI,UAAU,CAAC,kBAAkB,GAAG,aAAa;AACjD;AACA;AACA;AACA;AACA,0BAA0B,EAAE;;AC3B5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEO,MAAM,gBAAgB,SAASC,cAAS,CAAC;AAChD,IAAI,MAAM,cAAc,GAAG;AAC3B,QAAQ,MAAM,QAAQ,GAAG,qBAAqB,EAAE;AAChD;AACA;AACA;AACA,QAAQ,OAAO;AACf,YAAY,OAAO,EAAE,QAAQ,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI;AACpD,YAAY,MAAM,EAAE,YAAY;AAChC,SAAS;AACT,IAAI;AACJ,IAAI,MAAM,YAAY,GAAG;AACzB,QAAQ,MAAM,QAAQ,GAAG,qBAAqB,EAAE;AAChD,QAAQ,OAAO;AACf,YAAY,WAAW,EAAE,QAAQ,KAAK,IAAI;AAC1C,YAAY,aAAa,EAAE,QAAQ,KAAK,IAAI;AAC5C,YAAY,MAAM,EAAE,iBAAiB;AACrC,SAAS;AACT,IAAI;AACJ;AACA,SAAS,qBAAqB,GAAG;AACjC,IAAI,IAAI;AACR,QAAQ,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS;AACxC,QAAQ,MAAM,QAAQ,GAAG,GAAG,EAAE,UAAU,EAAE,QAAQ;AAClD,QAAQ,OAAO,OAAO,QAAQ,KAAK,SAAS,GAAG,QAAQ,GAAG,IAAI;AAC9D,IAAI;AACJ,IAAI,MAAM;AACV,QAAQ,OAAO,IAAI;AACnB,IAAI;AACJ;;;;;;;;;;"}
package/dist/plugin.js ADDED
@@ -0,0 +1,83 @@
1
+ var capacitorNetworkPolicy = (function (exports, core) {
2
+ 'use strict';
3
+
4
+ /**
5
+ * `@elizaos/capacitor-network-policy` — Android `metered` / iOS `isExpensive`
6
+ * shims for the voice-model auto-updater network policy (R5-versioning §4).
7
+ *
8
+ * Registers as `ElizaNetworkPolicy` on the Capacitor bridge. The
9
+ * platform-agnostic probes in `plugin-local-inference` read the bridge
10
+ * via `globalThis.ElizaNetworkPolicy` so this module's only job is to
11
+ * register and install the global.
12
+ */
13
+ const loadWeb = () => Promise.resolve().then(function () { return web; }).then((m) => new m.NetworkPolicyWeb());
14
+ const NetworkPolicy = core.registerPlugin("ElizaNetworkPolicy", { web: loadWeb });
15
+ /**
16
+ * Install `globalThis.ElizaNetworkPolicy` so the runtime probe in
17
+ * `plugin-local-inference/src/services/network-policy.ts` can call into
18
+ * the native bridge without compile-time Capacitor dependencies.
19
+ *
20
+ * Idempotent — re-installing replaces the existing handle with the same
21
+ * `NetworkPolicy` instance.
22
+ */
23
+ function installNetworkPolicyGlobal() {
24
+ globalThis.ElizaNetworkPolicy = NetworkPolicy;
25
+ }
26
+ // Side-effect: install on import so callers that simply
27
+ // `import "@elizaos/capacitor-network-policy"` from the app bootstrap
28
+ // pick up the global without any extra wiring.
29
+ installNetworkPolicyGlobal();
30
+
31
+ /**
32
+ * Web fallback for `@elizaos/capacitor-network-policy`.
33
+ *
34
+ * Browsers can read `navigator.connection.saveData` (a heuristic for "user
35
+ * has Data Saver enabled") and `.type` (`cellular` / `wifi` / `ethernet`),
36
+ * but the WICG spec is partial and Firefox/Safari support is limited. The
37
+ * fallback returns conservative defaults so the policy decision falls
38
+ * through to `unknown → ask` — never silently accepts a download on a
39
+ * potentially-metered link.
40
+ */
41
+ class NetworkPolicyWeb extends core.WebPlugin {
42
+ async getMeteredHint() {
43
+ const saveData = readNavigatorSaveData();
44
+ // `saveData=true` is a strong signal the user wants metered-mode behavior.
45
+ // We don't infer "metered=false" from `saveData=false` — that just means
46
+ // Data Saver isn't on, which says nothing about metering.
47
+ return {
48
+ metered: saveData === true ? true : null,
49
+ source: "android-os",
50
+ };
51
+ }
52
+ async getPathHints() {
53
+ const saveData = readNavigatorSaveData();
54
+ return {
55
+ isExpensive: saveData === true,
56
+ isConstrained: saveData === true,
57
+ source: "nw-path-monitor",
58
+ };
59
+ }
60
+ }
61
+ function readNavigatorSaveData() {
62
+ try {
63
+ const nav = globalThis.navigator;
64
+ const saveData = nav?.connection?.saveData;
65
+ return typeof saveData === "boolean" ? saveData : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ var web = /*#__PURE__*/Object.freeze({
73
+ __proto__: null,
74
+ NetworkPolicyWeb: NetworkPolicyWeb
75
+ });
76
+
77
+ exports.NetworkPolicy = NetworkPolicy;
78
+ exports.installNetworkPolicyGlobal = installNetworkPolicyGlobal;
79
+
80
+ return exports;
81
+
82
+ })({}, capacitorExports);
83
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["/**\n * `@elizaos/capacitor-network-policy` — Android `metered` / iOS `isExpensive`\n * shims for the voice-model auto-updater network policy (R5-versioning §4).\n *\n * Registers as `ElizaNetworkPolicy` on the Capacitor bridge. The\n * platform-agnostic probes in `plugin-local-inference` read the bridge\n * via `globalThis.ElizaNetworkPolicy` so this module's only job is to\n * register and install the global.\n */\nimport { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.NetworkPolicyWeb());\nexport const NetworkPolicy = registerPlugin(\"ElizaNetworkPolicy\", { web: loadWeb });\n/**\n * Install `globalThis.ElizaNetworkPolicy` so the runtime probe in\n * `plugin-local-inference/src/services/network-policy.ts` can call into\n * the native bridge without compile-time Capacitor dependencies.\n *\n * Idempotent — re-installing replaces the existing handle with the same\n * `NetworkPolicy` instance.\n */\nexport function installNetworkPolicyGlobal() {\n globalThis.ElizaNetworkPolicy = NetworkPolicy;\n}\n// Side-effect: install on import so callers that simply\n// `import \"@elizaos/capacitor-network-policy\"` from the app bootstrap\n// pick up the global without any extra wiring.\ninstallNetworkPolicyGlobal();\n//# sourceMappingURL=index.js.map","/**\n * Web fallback for `@elizaos/capacitor-network-policy`.\n *\n * Browsers can read `navigator.connection.saveData` (a heuristic for \"user\n * has Data Saver enabled\") and `.type` (`cellular` / `wifi` / `ethernet`),\n * but the WICG spec is partial and Firefox/Safari support is limited. The\n * fallback returns conservative defaults so the policy decision falls\n * through to `unknown → ask` — never silently accepts a download on a\n * potentially-metered link.\n */\nimport { WebPlugin } from \"@capacitor/core\";\nexport class NetworkPolicyWeb extends WebPlugin {\n async getMeteredHint() {\n const saveData = readNavigatorSaveData();\n // `saveData=true` is a strong signal the user wants metered-mode behavior.\n // We don't infer \"metered=false\" from `saveData=false` — that just means\n // Data Saver isn't on, which says nothing about metering.\n return {\n metered: saveData === true ? true : null,\n source: \"android-os\",\n };\n }\n async getPathHints() {\n const saveData = readNavigatorSaveData();\n return {\n isExpensive: saveData === true,\n isConstrained: saveData === true,\n source: \"nw-path-monitor\",\n };\n }\n}\nfunction readNavigatorSaveData() {\n try {\n const nav = globalThis.navigator;\n const saveData = nav?.connection?.saveData;\n return typeof saveData === \"boolean\" ? saveData : null;\n }\n catch {\n return null;\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IAAA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAGA,MAAM,OAAO,GAAG,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;AAC/D,UAAC,aAAa,GAAGA,mBAAc,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE;IAClF;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACO,SAAS,0BAA0B,GAAG;IAC7C,IAAI,UAAU,CAAC,kBAAkB,GAAG,aAAa;IACjD;IACA;IACA;IACA;IACA,0BAA0B,EAAE;;IC3B5B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEO,MAAM,gBAAgB,SAASC,cAAS,CAAC;IAChD,IAAI,MAAM,cAAc,GAAG;IAC3B,QAAQ,MAAM,QAAQ,GAAG,qBAAqB,EAAE;IAChD;IACA;IACA;IACA,QAAQ,OAAO;IACf,YAAY,OAAO,EAAE,QAAQ,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI;IACpD,YAAY,MAAM,EAAE,YAAY;IAChC,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,YAAY,GAAG;IACzB,QAAQ,MAAM,QAAQ,GAAG,qBAAqB,EAAE;IAChD,QAAQ,OAAO;IACf,YAAY,WAAW,EAAE,QAAQ,KAAK,IAAI;IAC1C,YAAY,aAAa,EAAE,QAAQ,KAAK,IAAI;IAC5C,YAAY,MAAM,EAAE,iBAAiB;IACrC,SAAS;IACT,IAAI;IACJ;IACA,SAAS,qBAAqB,GAAG;IACjC,IAAI,IAAI;IACR,QAAQ,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS;IACxC,QAAQ,MAAM,QAAQ,GAAG,GAAG,EAAE,UAAU,EAAE,QAAQ;IAClD,QAAQ,OAAO,OAAO,QAAQ,KAAK,SAAS,GAAG,QAAQ,GAAG,IAAI;IAC9D,IAAI;IACJ,IAAI,MAAM;IACV,QAAQ,OAAO,IAAI;IACnB,IAAI;IACJ;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,70 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import Network
4
+
5
+ /// Native iOS implementation of the `ElizaNetworkPolicy` Capacitor plugin
6
+ /// (R5-versioning §4.2).
7
+ ///
8
+ /// Bridges `NWPathMonitor` to the TypeScript `NetworkPolicyPlugin`
9
+ /// interface. The voice-model auto-updater calls `getPathHints()` before
10
+ /// every download to gate on `isExpensive` (Apple's "treat as metered"
11
+ /// flag) and `isConstrained` (Low Data Mode).
12
+ ///
13
+ /// Implementation note: the plugin keeps one long-lived `NWPathMonitor`
14
+ /// instance and reads its `currentPath` on each call. This avoids the
15
+ /// cost of starting/stopping a monitor per request and gives us a "live"
16
+ /// path snapshot — `NWPathMonitor.currentPath` is updated as the OS sees
17
+ /// path changes (cellular ↔ Wi-Fi handover, hotspot toggle, etc.).
18
+ @objc(ElizaNetworkPolicyPlugin)
19
+ public class ElizaNetworkPolicyPlugin: CAPPlugin, CAPBridgedPlugin {
20
+ public let identifier = "ElizaNetworkPolicyPlugin"
21
+ public let jsName = "ElizaNetworkPolicy"
22
+ public let pluginMethods: [CAPPluginMethod] = [
23
+ CAPPluginMethod(name: "getMeteredHint", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "getPathHints", returnType: CAPPluginReturnPromise),
25
+ ]
26
+
27
+ private let monitor = NWPathMonitor()
28
+ private let monitorQueue = DispatchQueue(label: "ai.eliza.network-policy.monitor", qos: .utility)
29
+ private var started = false
30
+
31
+ public override func load() {
32
+ startIfNeeded()
33
+ }
34
+
35
+ deinit {
36
+ if started {
37
+ monitor.cancel()
38
+ }
39
+ }
40
+
41
+ private func startIfNeeded() {
42
+ if started { return }
43
+ // The `pathUpdateHandler` is intentionally empty — we only read
44
+ // `monitor.currentPath` on demand. The handler is required for the
45
+ // monitor to publish path updates internally.
46
+ monitor.pathUpdateHandler = { _ in }
47
+ monitor.start(queue: monitorQueue)
48
+ started = true
49
+ }
50
+
51
+ /// Android-only safe fallback on iOS. Always resolves with the shared
52
+ /// response shape so the JS bridge can call `getMeteredHint()` uniformly across
53
+ /// platforms; iOS callers should prefer `getPathHints()`.
54
+ @objc func getMeteredHint(_ call: CAPPluginCall) {
55
+ var response: [String: Any] = ["source": "android-os"]
56
+ response["metered"] = NSNull()
57
+ call.resolve(response)
58
+ }
59
+
60
+ @objc func getPathHints(_ call: CAPPluginCall) {
61
+ startIfNeeded()
62
+ let path = monitor.currentPath
63
+ let response: [String: Any] = [
64
+ "isExpensive": path.isExpensive,
65
+ "isConstrained": path.isConstrained,
66
+ "source": "nw-path-monitor",
67
+ ]
68
+ call.resolve(response)
69
+ }
70
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@elizaos/capacitor-network-policy",
3
+ "version": "2.0.11-beta.7",
4
+ "description": "Android + iOS native shims surfacing the `metered` / `isExpensive` hints used by the voice-model auto-updater (R5-versioning §4).",
5
+ "main": "./dist/plugin.cjs.js",
6
+ "module": "./dist/esm/index.js",
7
+ "types": "./dist/esm/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/esm/index.d.ts",
11
+ "bun": "./src/index.ts",
12
+ "development": "./src/index.ts",
13
+ "import": "./dist/esm/index.js",
14
+ "require": "./dist/plugin.cjs.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "android/src/main/",
20
+ "android/build.gradle",
21
+ "ios/Sources/",
22
+ "ios/Package.swift",
23
+ "ElizaosCapacitorNetworkPolicy.podspec",
24
+ "dist/",
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "node ../../packages/scripts/with-package-build-lock.mjs plugins/plugin-native-network-policy -- bun run build:unlocked",
29
+ "clean": "node ../../packages/scripts/rm-path-recursive.mjs dist",
30
+ "test": "vitest run",
31
+ "prepublishOnly": "bun run build",
32
+ "build:unlocked": "bun run clean && tsc && bunx rollup -c rollup.config.mjs"
33
+ },
34
+ "license": "MIT",
35
+ "capacitor": {
36
+ "android": {
37
+ "src": "android"
38
+ },
39
+ "ios": {
40
+ "src": "ios"
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@capacitor/core": "^8.3.1",
45
+ "rollup": "^4.60.2",
46
+ "typescript": "^6.0.3",
47
+ "vitest": "^4.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@capacitor/core": "^8.3.1"
51
+ },
52
+ "elizaos": {
53
+ "platforms": [
54
+ "browser",
55
+ "node"
56
+ ],
57
+ "runtime": "both",
58
+ "platformDetails": {
59
+ "browser": "Web fallback returns metered=null / isExpensive=false so callers downgrade to ask.",
60
+ "node": "Server-side consumers read the bridge through the runtime probe; no Capacitor required."
61
+ }
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "gitHead": "cdbc876f793d96073d7eb0d09715a031ce0cd32e"
67
+ }