@adep/cli 0.1.16 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +101 -12
  2. package/dist/index.js +27134 -10430
  3. package/dist/vite.js +298 -35
  4. package/package.json +8 -6
  5. package/templates/app-desktop/README.md +51 -0
  6. package/templates/app-desktop/main.mjs +108 -0
  7. package/templates/app-desktop/offline-runtime.mjs +129 -0
  8. package/templates/app-desktop/package.json +49 -0
  9. package/templates/app-desktop/preload.cjs +45 -0
  10. package/templates/app-desktop/template.json +14 -0
  11. package/templates/app-desktop/window.mjs +53 -0
  12. package/templates/app-mobile/README.md +89 -0
  13. package/templates/app-mobile/adep.permissions.json +4 -0
  14. package/templates/app-mobile/bridge/gate.mjs +88 -0
  15. package/templates/app-mobile/capacitor.config.ts +23 -0
  16. package/templates/app-mobile/package.json +24 -0
  17. package/templates/app-mobile/plugins/adep-bridge/android/build.gradle +22 -0
  18. package/templates/app-mobile/plugins/adep-bridge/android/src/main/AndroidManifest.xml +9 -0
  19. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepBridge.kt +86 -0
  20. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepKeystore.kt +81 -0
  21. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepPermissionGate.kt +81 -0
  22. package/templates/app-mobile/plugins/adep-bridge/capacitor.plugin.json +10 -0
  23. package/templates/app-mobile/plugins/adep-bridge/dist/plugin.js +44 -0
  24. package/templates/app-mobile/plugins/adep-bridge/ios/AdepBridge.podspec +14 -0
  25. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepBridge.swift +69 -0
  26. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepKeychain.swift +58 -0
  27. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepPermissionGate.swift +82 -0
  28. package/templates/app-mobile/plugins/adep-bridge/package.json +14 -0
  29. package/templates/app-mobile/template.json +16 -0
  30. package/templates/app-mobile/web/adep-bridge.js +56 -0
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ AdepBridge 插件(library 模块)**不声明任何系统权限**(uses-permission 留空)。
4
+ 门控语义(PRD §2.11.5):未在应用 manifest 的 permissions 声明的能力即不可用,
5
+ 且不得在运行时申请——插件本体不主动索取任何系统权限;应用声明了哪些权限,
6
+ 由壳工程在 cap add 生成的应用级 AndroidManifest.xml 里配相应 uses-permission
7
+ (README 有映射指引)。
8
+ -->
9
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,86 @@
1
+ package com.adep.bridge
2
+
3
+ import com.getcapacitor.Plugin
4
+ import com.getcapacitor.PluginCall
5
+ import com.getcapacitor.PluginMethod
6
+ import com.getcapacitor.annotation.CapacitorPlugin
7
+
8
+ /**
9
+ * AgentDeploy 移动壳原生桥(APP-009,Android 侧)。
10
+ *
11
+ * 与 iOS 侧 AdepBridge.swift 同形(§3.2.8 硬约束④),与 bridge/gate.mjs 的 host-side
12
+ * 镜像同规则:**门控做在原生侧**——页面侧 `window.__ADEP_APP__.invokeNative` 经 Capacitor
13
+ * 消息进到这里,先过 [AdepPermissionGate],未在 `adep.permissions.json` 声明的能力直接
14
+ * 拒绝 `PERMISSION_DENIED` 并记审计,绝不在运行时申请。
15
+ *
16
+ * ownerKey 只存 Android Keystore([AdepKeystore]),明文不落 JS 侧 / 明文文件(§2.11.5)。
17
+ */
18
+ @CapacitorPlugin(name = "AdepBridge")
19
+ class AdepBridge : Plugin() {
20
+ private var gate: AdepPermissionGate? = null
21
+
22
+ override fun load() {
23
+ gate = try {
24
+ AdepPermissionGate.load(context)
25
+ } catch (error: Exception) {
26
+ // fail-closed:清单缺失 = 门控无法建立 = 一切原生能力不可用。
27
+ // 与 iOS 侧同口径:「清单没读到」≠「清单为空」。
28
+ android.util.Log.e("adep-bridge", "权限清单加载失败", error)
29
+ null
30
+ }
31
+ }
32
+
33
+ /** 调用原生能力。门控先于一切实现:未声明 → 拒绝 + 审计;已声明未实现 → NOT_IMPLEMENTED。 */
34
+ @PluginMethod
35
+ fun invokeNative(call: PluginCall) {
36
+ val capability = call.getString("capability")
37
+ if (capability.isNullOrEmpty()) {
38
+ call.resolve(jsObject("ok" to false, "code" to "INVALID_CAPABILITY", "message" to "capability 必须为非空字符串"))
39
+ return
40
+ }
41
+ val gate = gate ?: run {
42
+ call.resolve(jsObject("ok" to false, "code" to "GATE_UNAVAILABLE", "message" to "权限门控未建立(清单缺失)"))
43
+ return
44
+ }
45
+ when (gate.check(capability)) {
46
+ true -> call.resolve(
47
+ jsObject(
48
+ "ok" to false,
49
+ "code" to "NOT_IMPLEMENTED",
50
+ "message" to "原生能力 \"$capability\" 已声明但本壳未实现"
51
+ )
52
+ )
53
+ false -> call.resolve(
54
+ jsObject(
55
+ "ok" to false,
56
+ "code" to AdepPermissionGate.PERMISSION_DENIED,
57
+ "message" to "原生能力 \"$capability\" 未在应用 manifest 的 permissions 中声明(未声明即不可用,且不会在运行时申请)"
58
+ )
59
+ )
60
+ }
61
+ }
62
+
63
+ /** 读 ownerKey(Keystore)。无则返回 null(首启未签发)。 */
64
+ @PluginMethod
65
+ fun getOwnerKey(call: PluginCall) {
66
+ call.resolve(jsObject("value" to AdepKeystore.loadOwnerKey(context)))
67
+ }
68
+
69
+ /** 写 ownerKey(Keystore)。安装首启时由壳原生侧调用服务端签发端点后存入。 */
70
+ @PluginMethod
71
+ fun setOwnerKey(call: PluginCall) {
72
+ val key = call.getString("key")
73
+ if (key.isNullOrEmpty()) {
74
+ call.reject("key 必须为非空字符串")
75
+ return
76
+ }
77
+ AdepKeystore.saveOwnerKey(context, key)
78
+ call.resolve()
79
+ }
80
+
81
+ private fun jsObject(vararg pairs: Pair<String, Any?>): org.json.JSONObject {
82
+ val object = org.json.JSONObject()
83
+ for ((key, value) in pairs) object.put(key, value)
84
+ return object
85
+ }
86
+ }
@@ -0,0 +1,81 @@
1
+ package com.adep.bridge
2
+
3
+ import android.content.Context
4
+ import android.security.keystore.KeyGenParameterSpec
5
+ import android.security.keystore.KeyProperties
6
+ import android.util.Base64
7
+ import java.security.KeyStore
8
+ import javax.crypto.Cipher
9
+ import javax.crypto.KeyGenerator
10
+ import javax.crypto.SecretKey
11
+ import javax.crypto.spec.GCMParameterSpec
12
+
13
+ /**
14
+ * ownerKey 的 Keystore 加密存储(Android,APP-009)。
15
+ *
16
+ * PRD §2.11.5「设备身份与恢复」:ownerKey 移动端**只存 Keychain / Keystore**——不落
17
+ * 明文 SharedPreferences / 文件(客户端产物等同公开,明文落盘等于把设备身份交给解包者)。
18
+ *
19
+ * 实现:Android Keystore 系统生成/持有 AES 密钥(不可导出),ownerKey 用 AES/GCM 加密后
20
+ * 存 SharedPreferences 密文。密钥本身留在 Keystore(硬件/系统隔离),应用进程只能用它
21
+ * 加解密、拿不到密钥材料。
22
+ */
23
+ object AdepKeystore {
24
+ private const val KEYSTORE = "AndroidKeyStore"
25
+ private const val KEY_ALIAS = "adep_owner_key"
26
+ private const val PREFS_NAME = "adep_keystore"
27
+ private const val PREFS_CIPHERTEXT = "owner_key_ciphertext"
28
+ private const val PREFS_IV = "owner_key_iv"
29
+ private const val GCM_TAG_BITS = 128
30
+
31
+ /** 读 ownerKey;无则返回 null(首启未签发)。 */
32
+ fun loadOwnerKey(context: Context): String? {
33
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
34
+ val ciphertext = prefs.getString(PREFS_CIPHERTEXT, null) ?: return null
35
+ val iv = prefs.getString(PREFS_IV, null) ?: return null
36
+ return try {
37
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
38
+ cipher.init(
39
+ Cipher.DECRYPT_MODE,
40
+ loadOrCreateKey(),
41
+ GCMParameterSpec(GCM_TAG_BITS, Base64.decode(iv, Base64.NO_WRAP)),
42
+ )
43
+ String(cipher.doFinal(Base64.decode(ciphertext, Base64.NO_WRAP)), Charsets.UTF_8)
44
+ } catch (error: Exception) {
45
+ android.util.Log.e("adep-bridge", "ownerKey 解密失败", error)
46
+ null
47
+ }
48
+ }
49
+
50
+ /** 写 ownerKey(覆盖旧值)。 */
51
+ fun saveOwnerKey(context: Context, key: String) {
52
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
53
+ cipher.init(Cipher.ENCRYPT_MODE, loadOrCreateKey())
54
+ val ciphertext = cipher.doFinal(key.toByteArray(Charsets.UTF_8))
55
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
56
+ .putString(PREFS_CIPHERTEXT, Base64.encodeToString(ciphertext, Base64.NO_WRAP))
57
+ .putString(PREFS_IV, Base64.encodeToString(cipher.iv, Base64.NO_WRAP))
58
+ .apply()
59
+ }
60
+
61
+ /** 删 ownerKey(换装 / 恢复码认领时用)。 */
62
+ fun deleteOwnerKey(context: Context) {
63
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit().clear().apply()
64
+ }
65
+
66
+ private fun loadOrCreateKey(): SecretKey {
67
+ val store = KeyStore.getInstance(KEYSTORE).apply { load(null) }
68
+ (store.getKey(KEY_ALIAS, null) as? SecretKey)?.let { return it }
69
+ val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE)
70
+ generator.init(
71
+ KeyGenParameterSpec.Builder(
72
+ KEY_ALIAS,
73
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
74
+ )
75
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
76
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
77
+ .build(),
78
+ )
79
+ return generator.generateKey()
80
+ }
81
+ }
@@ -0,0 +1,81 @@
1
+ package com.adep.bridge
2
+
3
+ import android.content.Context
4
+ import org.json.JSONObject
5
+ import java.io.File
6
+ import java.text.SimpleDateFormat
7
+ import java.util.Date
8
+ import java.util.Locale
9
+
10
+ /**
11
+ * 原生侧权限门控(Android,APP-009)。
12
+ *
13
+ * 与 `bridge/gate.mjs`(host-side harness 镜像)**同规则**:`permissions` 白名单 contains
14
+ * 判定 + 审计记录。两侧独立实现、共用同一份清单契约(`adep.permissions.json`)与同一
15
+ * 错误码 `PERMISSION_DENIED`。规则刻意极简(没有第二种判定形态),漂移面最小;
16
+ * 模板单测钉住「Kotlin 源码引用同一清单文件名与错误码」。
17
+ */
18
+ class AdepPermissionGate private constructor(
19
+ private val permissions: Set<String>,
20
+ private val audit: AdepAuditLog,
21
+ ) {
22
+ companion object {
23
+ /** 与 bridge/gate.mjs 共用的契约常量。 */
24
+ const val PERMISSION_DENIED = "PERMISSION_DENIED"
25
+ /** 与 CLI 写入文件名一致的契约常量(app/site/adep.permissions.json → cap sync → assets/public/)。 */
26
+ const val MANIFEST_FILE_NAME = "adep.permissions.json"
27
+
28
+ /**
29
+ * 工厂:从 assets 读权限清单装配门控。
30
+ * 清单缺失 / 非 JSON → throw(调用方 fail-closed:门控建立不了 = 一切原生能力不可用)。
31
+ */
32
+ fun load(context: Context): AdepPermissionGate {
33
+ val raw: String = try {
34
+ context.assets.open("public/$MANIFEST_FILE_NAME").bufferedReader().use { it.readText() }
35
+ } catch (error: Exception) {
36
+ throw IllegalStateException(
37
+ "assets 缺少 public/$MANIFEST_FILE_NAME(CLI build 写入、cap sync 同步)",
38
+ error,
39
+ )
40
+ }
41
+ val permissions: Set<String> = try {
42
+ JSONObject(raw).optJSONArray("permissions")?.let { array ->
43
+ buildSet {
44
+ for (i in 0 until array.length()) {
45
+ array.optString(i).takeIf { it.isNotEmpty() }?.let { add(it) }
46
+ }
47
+ }
48
+ } ?: emptySet()
49
+ } catch (error: Exception) {
50
+ throw IllegalStateException("adep.permissions.json 不是合法 JSON", error)
51
+ }
52
+ return AdepPermissionGate(permissions, AdepAuditLog(context))
53
+ }
54
+ }
55
+
56
+ /** 声明面判定 + 审计(尝试即记,无论放行还是拒绝)。返回是否已声明。 */
57
+ fun check(capability: String): Boolean {
58
+ val declared = permissions.contains(capability)
59
+ audit.log(
60
+ ts = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US).format(Date()),
61
+ capability = capability,
62
+ result = if (declared) "granted" else "denied",
63
+ reason = if (declared) "ok" else "not_declared",
64
+ )
65
+ return declared
66
+ }
67
+ }
68
+
69
+ /** 设备侧审计日志(Android):filesDir/adep-audit.log 追加一行,与 gate.mjs 的 AuditEntry 同形。 */
70
+ class AdepAuditLog(private val context: Context) {
71
+ private val file: File = File(context.filesDir, "adep-audit.log")
72
+
73
+ fun log(ts: String, capability: String, result: String, reason: String) {
74
+ val line = "{\"ts\":\"$ts\",\"capability\":\"$capability\",\"result\":\"$result\",\"reason\":\"$reason\"}\n"
75
+ try {
76
+ file.appendText(line)
77
+ } catch (_: Exception) {
78
+ // 审计写失败不阻断门控判定(设备侧日志尽力而为,不因日志拖垮能力调用)。
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "ios": {
3
+ "src": "ios",
4
+ "dest": "ios"
5
+ },
6
+ "android": {
7
+ "src": "android",
8
+ "dest": "android"
9
+ }
10
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @adep/bridge —— AgentDeploy 移动壳原生桥的 JS 面(Capacitor 插件注册)。
3
+ *
4
+ * 页面侧经 `window.__ADEP_APP__.invokeNative(capability, args)` 进入(web/adep-bridge.js),
5
+ * 本模块把它转发给原生实现(AdepBridge.swift / AdepBridge.kt)。门控做在原生侧:
6
+ * 未在 `adep.permissions.json` 声明的能力 → 原生侧直接拒绝 `PERMISSION_DENIED` 并记审计,
7
+ * JS 面不持有任何放行决策(客户端产物等同公开,决策放 JS 侧等于没有门)。
8
+ *
9
+ * Web(非壳环境)fallback:非原生环境一律拒绝(fail-closed,与门控方向一致)。
10
+ */
11
+ import { registerPlugin } from '@capacitor/core'
12
+
13
+ const AdepBridge = registerPlugin('AdepBridge', {
14
+ /**
15
+ * 调用原生能力。返回 `{ ok: true, value }` 或 `{ ok: false, code, message }`;
16
+ * 未声明权限 → `code: 'PERMISSION_DENIED'`(与 bridge/gate.mjs 同一契约常量)。
17
+ */
18
+ invokeNative: (options) => {
19
+ const capability = options && options.capability
20
+ if (typeof capability !== 'string' || capability === '') {
21
+ return Promise.resolve({
22
+ ok: false,
23
+ code: 'INVALID_CAPABILITY',
24
+ message: 'capability 必须为非空字符串',
25
+ })
26
+ }
27
+ // Web fallback(浏览器预览 / 非壳环境):原生能力一律不可用,不做任何模拟。
28
+ return Promise.resolve({
29
+ ok: false,
30
+ code: 'NATIVE_UNAVAILABLE',
31
+ message: `原生能力 "${capability}" 在 Web 环境不可用(移动壳原生侧才提供)`,
32
+ })
33
+ },
34
+ // ownerKey 只存 Keychain / Keystore(PRD §2.11.5「设备身份与恢复」);JS 面只提供
35
+ // 存取接口,明文不过 JS 侧持久化。安装首启时由壳原生侧调用服务端签发端点换取。
36
+ getOwnerKey: () =>
37
+ Promise.resolve({
38
+ value: null,
39
+ reason: 'NATIVE_UNAVAILABLE',
40
+ }),
41
+ })
42
+
43
+ export { AdepBridge }
44
+ export default AdepBridge
@@ -0,0 +1,14 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'AdepBridge'
3
+ s.version = '0.1.0'
4
+ s.summary = 'AgentDeploy mobile shell native bridge (APP-009)'
5
+ s.description = 'Native-side permission gate + ownerKey Keychain storage + native capability invoke routing'
6
+ s.homepage = 'https://adep.jajabjbj.top'
7
+ s.license = 'MIT'
8
+ s.author = 'AgentDeploy'
9
+ s.source = { :git => 'https://adep.jajabjbj.top/adep-bridge.git', :tag => s.version.to_s }
10
+ s.ios.deployment_target = '13.0'
11
+ s.source_files = 'Sources/**/*.swift'
12
+ s.swift_version = '5.1'
13
+ s.dependency 'Capacitor'
14
+ end
@@ -0,0 +1,69 @@
1
+ import Capacitor
2
+
3
+ /// AgentDeploy 移动壳原生桥(APP-009,iOS 侧)。
4
+ ///
5
+ /// 职责(与 bridge/gate.mjs 的 host-side 镜像同规则;Android 侧 AdepBridge.kt 同形):
6
+ /// 1. 原生能力 invoke 路由:页面侧 `window.__ADEP_APP__.invokeNative(capability, args)`
7
+ /// 经 Capacitor 消息进到这里;**门控做在原生侧**——先过 `AdepPermissionGate`,
8
+ /// 未在 `adep.permissions.json` 声明的能力直接拒绝 `PERMISSION_DENIED` 并记审计,
9
+ /// 绝不在运行时申请(PRD §2.11.5)。
10
+ /// 2. ownerKey 只存 Keychain(`AdepKeychain`),明文不落 JS 侧 / 沙盒文件(§2.11.5
11
+ /// 「设备身份与恢复」)。
12
+ ///
13
+ /// 权限清单来源:CLI `adep app build` 把 `adep.permissions.json` 写入 `app/site/`;
14
+ /// `npx cap sync` 把它同步进 iOS 工程的 `Public/`(folder reference),运行时经
15
+ /// `Bundle.main` 读取。
16
+ @objc(AdepBridge)
17
+ public class AdepBridge: CAPPlugin {
18
+ private var gate: AdepPermissionGate?
19
+
20
+ public override func load() {
21
+ super.load()
22
+ do {
23
+ gate = try AdepPermissionGate.load(from: Bundle.main)
24
+ } catch {
25
+ // fail loud:清单缺失 = 门控无法建立 = 一切原生能力不可用(fail-closed)。
26
+ // 不静默放行——「清单没读到」与「清单为空」的后果必须不同(后者=未声明即不可用)。
27
+ gate = nil
28
+ CAPLog.print("[adep-bridge] 权限清单加载失败:\(error)")
29
+ }
30
+ }
31
+
32
+ /// 调用原生能力。门控先于一切实现:未声明 → 拒绝 + 审计;已声明 → 按能力分发。
33
+ /// 本模板交付「门控 + 桥框架」:实际能力实现(定位 / 相机等)由应用侧按能力名
34
+ /// 扩展,未实现的能力返回 `NOT_IMPLEMENTED`(不静默返回成功)。
35
+ @objc func invokeNative(_ call: CAPPluginCall) {
36
+ guard let capability = call.getString("capability"), !capability.isEmpty else {
37
+ call.resolve(["ok": false, "code": "INVALID_CAPABILITY", "message": "capability 必须为非空字符串"])
38
+ return
39
+ }
40
+ guard let gate = gate else {
41
+ call.resolve(["ok": false, "code": "GATE_UNAVAILABLE", "message": "权限门控未建立(清单缺失)"])
42
+ return
43
+ }
44
+ switch gate.check(capability) {
45
+ case .granted:
46
+ // 能力分发点:后续按 capability 接真实现(如 `geolocation` → CoreLocation)。
47
+ // 本模板不含任何真实原生能力实现——未实现 = 明确报错,不是假装成功。
48
+ call.resolve(["ok": false, "code": "NOT_IMPLEMENTED", "message": "原生能力 \"\(capability)\" 已声明但本壳未实现"])
49
+ case .denied(let reason):
50
+ call.resolve(["ok": false, "code": AdepPermissionGate.permissionDenied, "message": reason])
51
+ }
52
+ }
53
+
54
+ /// 读 ownerKey(Keychain)。无则返回 null(首启未签发)。
55
+ @objc func getOwnerKey(_ call: CAPPluginCall) {
56
+ let value = AdepKeychain.loadOwnerKey()
57
+ call.resolve(["value": value ?? NSNull()])
58
+ }
59
+
60
+ /// 写 ownerKey(Keychain)。安装首启时由壳原生侧调用服务端签发端点后存入。
61
+ @objc func setOwnerKey(_ call: CAPPluginCall) {
62
+ guard let key = call.getString("key"), !key.isEmpty else {
63
+ call.reject("key 必须为非空字符串")
64
+ return
65
+ }
66
+ AdepKeychain.saveOwnerKey(key)
67
+ call.resolve()
68
+ }
69
+ }
@@ -0,0 +1,58 @@
1
+ import Foundation
2
+ import Security
3
+
4
+ /// ownerKey 的 Keychain 存储(iOS,APP-009)。
5
+ ///
6
+ /// PRD §2.11.5「设备身份与恢复」:ownerKey 移动端**只存 Keychain / Keystore**——
7
+ /// 不落 UserDefaults / 沙盒文件 / JS 侧持久化(客户端产物等同公开,明文落盘等于
8
+ /// 把设备身份直接交给解包者)。Keychain 由系统加密保护,应用卸载后条目随钥匙串
9
+ /// 条目保留策略由系统管理(本实现用 kSecAttrAccessibleAfterFirstUnlock,设备
10
+ /// 重启后首次解锁即可读,适合后台同步场景)。
11
+ enum AdepKeychain {
12
+ private static let service = "adep.ownerKey"
13
+ private static let account = "default"
14
+
15
+ /// 读 ownerKey;无则返回 nil(首启未签发)。
16
+ static func loadOwnerKey() -> String? {
17
+ var query = baseQuery()
18
+ query[kSecReturnData as String] = true
19
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
20
+ var item: CFTypeRef?
21
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
22
+ guard status == errSecSuccess, let data = item as? Data else { return nil }
23
+ return String(data: data, encoding: .utf8)
24
+ }
25
+
26
+ /// 写 ownerKey(已存在则覆盖)。
27
+ @discardableResult
28
+ static func saveOwnerKey(_ key: String) -> Bool {
29
+ deleteOwnerKey()
30
+ var query = baseQuery()
31
+ query[kSecValueData as String] = Data(key.utf8)
32
+ return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
33
+ }
34
+
35
+ /// 删 ownerKey(换装 / 恢复码认领时用)。
36
+ @discardableResult
37
+ static func deleteOwnerKey() -> Bool {
38
+ SecItemDelete(baseQuery() as CFDictionary) == errSecSuccess || errSecItemNotFoundCheck()
39
+ }
40
+
41
+ private static func baseQuery() -> [String: Any] {
42
+ return [
43
+ kSecClass as String: kSecClassGenericPassword,
44
+ kSecAttrService as String: service,
45
+ kSecAttrAccount as String: account,
46
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
47
+ ]
48
+ }
49
+
50
+ /// delete 在「无条目」时返回 errSecItemNotFound,等同成功(幂等删除)。
51
+ private static func errSecItemNotFoundCheck() -> Bool {
52
+ // 上一条删除若因条目不存在失败,这里重查一次确认「确实不存在」即可算成功。
53
+ var query = baseQuery()
54
+ query[kSecReturnData as String] = true
55
+ var item: CFTypeRef?
56
+ return SecItemCopyMatching(query as CFDictionary, &item) == errSecItemNotFound
57
+ }
58
+ }
@@ -0,0 +1,82 @@
1
+ import Foundation
2
+
3
+ /// 原生侧权限门控(iOS,APP-009)。
4
+ ///
5
+ /// 与 `bridge/gate.mjs`(host-side harness 镜像)**同规则**:`permissions` 白名单 contains
6
+ /// 判定 + 审计记录。两侧独立实现、共用同一份清单契约(`adep.permissions.json`)与同一
7
+ /// 错误码 `PERMISSION_DENIED`。规则刻意极简(没有第二种判定形态),漂移面最小;
8
+ /// 模板单测钉住「Swift 源码引用同一清单文件名与错误码」。
9
+ public final class AdepPermissionGate {
10
+ /// 与 bridge/gate.mjs 共用的契约常量。
11
+ public static let permissionDenied = "PERMISSION_DENIED"
12
+ /// 与 CLI 写入文件名一致的契约常量(app/site/adep.permissions.json → cap sync → Public/)。
13
+ static let manifestFileName = "adep.permissions.json"
14
+
15
+ public enum Result {
16
+ case granted
17
+ case denied(reason: String)
18
+ }
19
+
20
+ private let permissions: [String]
21
+ private let audit: AdepAuditLog
22
+
23
+ private init(permissions: [String], audit: AdepAuditLog) {
24
+ self.permissions = permissions
25
+ self.audit = audit
26
+ }
27
+
28
+ /// 从 bundle 读权限清单并装配门控。
29
+ /// 清单缺失 → throw(调用方 fail-closed:门控建立不了 = 一切原生能力不可用)。
30
+ static func load(from bundle: Bundle) throws -> AdepPermissionGate {
31
+ guard let url = bundle.url(forResource: manifestFileName, withExtension: nil) else {
32
+ throw NSError(
33
+ domain: "AdepBridge", code: 1,
34
+ userInfo: [NSLocalizedDescriptionKey: "Bundle 缺少 \(manifestFileName)(CLI build 写入、cap sync 同步)"]
35
+ )
36
+ }
37
+ let data = try Data(contentsOf: url)
38
+ let object = try JSONSerialization.jsonObject(with: data) as? [String: Any]
39
+ let raw = object?["permissions"] as? [String] ?? []
40
+ return AdepPermissionGate(permissions: raw, audit: AdepAuditLog.shared)
41
+ }
42
+
43
+ /// 声明面判定 + 审计(尝试即记,无论放行还是拒绝)。
44
+ func check(_ capability: String) -> Result {
45
+ let declared = permissions.contains(capability)
46
+ audit.log(
47
+ ts: ISO8601DateFormatter().string(from: Date()),
48
+ capability: capability,
49
+ result: declared ? "granted" : "denied",
50
+ reason: declared ? "ok" : "not_declared"
51
+ )
52
+ if declared { return .granted }
53
+ return .denied(reason: "原生能力 \"\(capability)\" 未在应用 manifest 的 permissions 中声明(未声明即不可用,且不会在运行时申请)")
54
+ }
55
+ }
56
+
57
+ /// 设备侧审计日志(iOS):沙盒 Documents/adep-audit.log 追加一行。
58
+ /// 与 gate.mjs 的 AuditEntry 形状一致(ts / capability / result / reason)。
59
+ public final class AdepAuditLog {
60
+ static let shared = AdepAuditLog()
61
+
62
+ private let fileURL: URL
63
+
64
+ private init() {
65
+ let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
66
+ fileURL = documents.appendingPathComponent("adep-audit.log")
67
+ }
68
+
69
+ func log(ts: String, capability: String, result: String, reason: String) {
70
+ let line = "{\"ts\":\"\(ts)\",\"capability\":\"\(capability)\",\"result\":\"\(result)\",\"reason\":\"\(reason)\"}\n"
71
+ guard let data = line.data(using: .utf8) else { return }
72
+ if FileManager.default.fileExists(atPath: fileURL.path) {
73
+ if let handle = try? FileHandle(forWritingTo: fileURL) {
74
+ defer { try? handle.close() }
75
+ handle.seekToEndOfFile()
76
+ handle.write(data)
77
+ }
78
+ } else {
79
+ try? data.write(to: fileURL, options: .atomic)
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@adep/bridge",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "AgentDeploy 移动壳原生桥(APP-009):原生侧权限门控 + ownerKey 存储(Keychain / Keystore)+ 原生能力 invoke 路由",
6
+ "main": "dist/plugin.js",
7
+ "types": "dist/plugin.d.ts",
8
+ "scripts": {
9
+ "verify": "node -e \"require('./dist/plugin.js')\""
10
+ },
11
+ "peerDependencies": {
12
+ "@capacitor/core": "^7.0.0"
13
+ }
14
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "code": "app-mobile",
3
+ "name": "移动应用壳(Capacitor)",
4
+ "description": "Capacitor 移动壳工程:WebView 消费 app/site/ 产物,在线运行时(云函数经 platformUrl 服务端执行);原生桥(plugins/adep-bridge)承载原生侧权限门控与 ownerKey 存储(Keychain / Keystore)",
5
+ "version": "1.0.0",
6
+ "variables": [
7
+ {
8
+ "name": "projectSlug",
9
+ "description": "平台项目 slug(写入包名 / adep.app.json / bundle id 推导)"
10
+ },
11
+ { "name": "appName", "description": "应用显示名(iOS / Android 显示名)" },
12
+ { "name": "platformUrl", "description": "平台地址(页面侧 @adep/client 以此为 baseUrl invoke 云函数)" },
13
+ { "name": "iosBundleId", "description": "iOS bundle identifier(top.adep.<slug>,连字符合法)" },
14
+ { "name": "androidPackageId", "description": "Android applicationId(top.adep.<slug 连字符转下划线>,applicationId 不允许连字符)" }
15
+ ]
16
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 页面侧桥(APP-009 移动壳,与桌面壳 preload.cjs **同形**——§3.2.8 硬约束④:移动与桌面桥
3
+ * 同形,同一套页面代码不区分运行壳)。
4
+ *
5
+ * CLI `adep app build --platform ios,android` 时把本文件复制为 `app/site/adep-bridge.js`
6
+ * 并在 `index.html` 注入 `<script src="/adep-bridge.js"></script>`(幂等),同时把
7
+ * `adep.app.json` 复制到 `app/site/adep.app.json`。页面加载时本脚本异步读取配置并暴露
8
+ * `window.__ADEP_APP__`——与桌面 preload 暴露的字段一一对应。
9
+ *
10
+ * 本文件**自包含**(不 import 任何 npm 包):WebView 里 `@capacitor/core` 由页面 bundle
11
+ * 提供(`window.Capacitor` 运行时全局),壳不能假设 site 产物打包过它。
12
+ *
13
+ * 页面侧调用云函数(在线运行时)与桌面同款:
14
+ *
15
+ * const client = createAdepClient({ baseUrl: window.__ADEP_APP__.platformUrl })
16
+ * const result = await client.invokeFunction('hello', { name: 'world' })
17
+ *
18
+ * 设备端函数(离线运行时)请求原生能力:
19
+ *
20
+ * const result = await window.__ADEP_APP__.invokeNative('geolocation', {})
21
+ * // 未在 manifest permissions 声明 → 原生侧拒绝 { code: 'PERMISSION_DENIED' }
22
+ */
23
+ ;(async () => {
24
+ /** 壳配置(CLI build 时从壳工程 adep.app.json 复制进 site/;字段校验归 CLI schema)。 */
25
+ const response = await fetch('./adep.app.json')
26
+ const config = await response.json()
27
+
28
+ // 字符串 key 写法(与桌面 preload.cjs 同款):`__ADEP_APP__` 是壳与页面约定的全局名,
29
+ // 以标识符出现会触发 no-underscore-dangle(lint),字符串形式表达「这是契约名不是变量」。
30
+ window['__ADEP_APP__'] = {
31
+ platformUrl: config.platformUrl,
32
+ projectSlug: config.projectSlug,
33
+ appName: config.appName || config.projectSlug,
34
+ // 本模板交付在线运行时(PRD §2.11 路径 A:函数在服务端执行,改逻辑不需要重新发版)。
35
+ // 移动离线运行时(设备端执行 + 原生能力)是后续单;桥的 invokeNative 入口已就位,
36
+ // 离线注入接上后页面调用形态不变。
37
+ mode: 'online',
38
+ /**
39
+ * 原生能力入口:经 Capacitor 插件 `@adep/bridge`(plugins/adep-bridge)→ 原生侧门控。
40
+ * 门控做在原生侧:未声明权限即不可用(PERMISSION_DENIED)且不在运行时申请。
41
+ */
42
+ invokeNative: (capability, args) => {
43
+ const plugin =
44
+ window.Capacitor &&
45
+ window.Capacitor.Plugins &&
46
+ window.Capacitor.Plugins.AdepBridge
47
+ if (!plugin) {
48
+ return Promise.reject({
49
+ code: 'NATIVE_BRIDGE_UNAVAILABLE',
50
+ message: '原生桥不可用:页面未运行在 AdepBridge 注入的移动壳内',
51
+ })
52
+ }
53
+ return plugin.invokeNative({ capability, args })
54
+ },
55
+ }
56
+ })()