@adep/cli 0.1.17 → 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 (29) hide show
  1. package/README.md +96 -12
  2. package/dist/index.js +27103 -10486
  3. package/package.json +8 -6
  4. package/templates/app-desktop/README.md +51 -0
  5. package/templates/app-desktop/main.mjs +108 -0
  6. package/templates/app-desktop/offline-runtime.mjs +129 -0
  7. package/templates/app-desktop/package.json +49 -0
  8. package/templates/app-desktop/preload.cjs +45 -0
  9. package/templates/app-desktop/template.json +14 -0
  10. package/templates/app-desktop/window.mjs +53 -0
  11. package/templates/app-mobile/README.md +89 -0
  12. package/templates/app-mobile/adep.permissions.json +4 -0
  13. package/templates/app-mobile/bridge/gate.mjs +88 -0
  14. package/templates/app-mobile/capacitor.config.ts +23 -0
  15. package/templates/app-mobile/package.json +24 -0
  16. package/templates/app-mobile/plugins/adep-bridge/android/build.gradle +22 -0
  17. package/templates/app-mobile/plugins/adep-bridge/android/src/main/AndroidManifest.xml +9 -0
  18. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepBridge.kt +86 -0
  19. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepKeystore.kt +81 -0
  20. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepPermissionGate.kt +81 -0
  21. package/templates/app-mobile/plugins/adep-bridge/capacitor.plugin.json +10 -0
  22. package/templates/app-mobile/plugins/adep-bridge/dist/plugin.js +44 -0
  23. package/templates/app-mobile/plugins/adep-bridge/ios/AdepBridge.podspec +14 -0
  24. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepBridge.swift +69 -0
  25. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepKeychain.swift +58 -0
  26. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepPermissionGate.swift +82 -0
  27. package/templates/app-mobile/plugins/adep-bridge/package.json +14 -0
  28. package/templates/app-mobile/template.json +16 -0
  29. package/templates/app-mobile/web/adep-bridge.js +56 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * 权限门控核心(APP-009,PRD §2.11.5「原生能力与权限」)。
3
+ *
4
+ * 「未声明即不可用,且不得在运行时申请」的门控**做在原生侧**(ADR-0007 配套硬约束 3):
5
+ * iOS Swift / Android Kotlin 的原生桥持有一份「已声明权限」清单,任何原生能力调用先过门控。
6
+ *
7
+ * 本文件是这条判定的 **host-side harness 镜像**(PRD §2.11.6 前言:可判定部分全部打在
8
+ * 引擎无关的 host-side harness 上,CI 用同一份设备端代码跑判定;真机只冒烟)。CI 没有
9
+ * iOS / Android 工具链,无法编译原生侧,因此用本文件在同一输入面上跑「未声明 → 拒绝 +
10
+ * 审计日志」的验收判据。
11
+ *
12
+ * ⚠️ **双实现漂移防线**(AGENTS §9:同一算法两端各存一份拷贝必须点名分叉并双向对账):
13
+ * 原生桥与这里各自实现**同一规则**——`permissions` 白名单的 contains 判定 + 审计记录。
14
+ * 规则刻意极简(没有第二种判定形态),两侧共用同一份清单契约
15
+ * (`adep.permissions.json`,CLI build 时从服务端打包配置写入 `app/site/`)与同一错误码
16
+ * `PERMISSION_DENIED`。模板单测钉住「原生侧源码引用同一文件契约与同一错误码」,
17
+ * 防止一侧改规则另一侧不同步(漂移方向 = 实际放行面比声明面大)。
18
+ *
19
+ * 本文件**不**触碰系统权限 API(那是原生侧的事):已声明但系统未授权由原生侧返回
20
+ * 系统状态码;这里只做「声明面」判定与审计。设备端函数(离线运行时)经原生桥调用
21
+ * 原生能力时,两侧门控按同一规则先后拦截。
22
+ *
23
+ * 注意:本文件是随壳工程分发的 **.mjs**(设备端 / host-side 都直接跑),不写 TypeScript
24
+ * 语法;类型以 JSDoc 表达。
25
+ */
26
+
27
+ /** 未声明权限的机器码(原生侧与 JS 侧共用的契约常量)。 */
28
+ export const PERMISSION_DENIED = 'PERMISSION_DENIED'
29
+
30
+ /**
31
+ * 声明面判定:`capability` 是否在权限清单中。
32
+ *
33
+ * @param {readonly string[]} permissions 已声明权限清单(adep.permissions.json 的 permissions 数组)
34
+ * @param {string} capability 原生能力名(如 geolocation / camera / filesystem.read)
35
+ * @returns {boolean}
36
+ */
37
+ export function isPermissionDeclared(permissions, capability) {
38
+ return Array.isArray(permissions) && permissions.includes(capability)
39
+ }
40
+
41
+ /**
42
+ * 内存审计 sink(host-side harness 用;每次调用追加一条)。
43
+ * 审计条目:`{ ts, capability, result, reason }`——拒绝原因固定为 `not_declared`(未声明),
44
+ * 该次尝试无论成功与否都留痕(审计不是「拒绝才记」,是「尝试即记」)。
45
+ *
46
+ * @returns {object} `{ sink, entries }`——sink.log(entry) 追加审计;entries 为已记条目数组。
47
+ * entry 形状:`{ ts, capability, result, reason }`,result 为 'granted' | 'denied',
48
+ * reason 为 'not_declared' | 'ok'。
49
+ */
50
+ export function createMemoryAudit() {
51
+ const entries = []
52
+ return {
53
+ sink: {
54
+ log(entry) {
55
+ entries.push(entry)
56
+ },
57
+ },
58
+ entries,
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 权限门控(由权限清单 + 审计 sink 装配;CLI 构建期生成清单,运行时装配门控)。
64
+ *
65
+ * @param {object} input 门控装配输入——`permissions`(readonly string[]:已声明权限清单)、
66
+ * `audit`(含 `log(entry)` 方法的审计 sink)。
67
+ * @returns {object} `{ check(capability) }`——check 返回 `{ ok: true }` 或
68
+ * `{ ok: false, code: 'PERMISSION_DENIED', message }`。
69
+ */
70
+ export function createPermissionGate(input) {
71
+ return {
72
+ check(capability) {
73
+ const declared = isPermissionDeclared(input.permissions, capability)
74
+ input.audit.log({
75
+ ts: new Date().toISOString(),
76
+ capability,
77
+ result: declared ? 'granted' : 'denied',
78
+ reason: declared ? 'ok' : 'not_declared',
79
+ })
80
+ if (declared) return { ok: true }
81
+ return {
82
+ ok: false,
83
+ code: PERMISSION_DENIED,
84
+ message: `原生能力 "${capability}" 未在应用 manifest 的 permissions 中声明(未声明即不可用,且不会在运行时申请)`,
85
+ }
86
+ },
87
+ }
88
+ }
@@ -0,0 +1,23 @@
1
+ import type { CapacitorConfig } from '@capacitor/cli'
2
+
3
+ /**
4
+ * Capacitor 配置(APP-009 移动壳模板)。
5
+ *
6
+ * - `appId`:iOS bundle identifier(`{{iosBundleId}}`)。Android 的 applicationId 在
7
+ * `npx cap add android` 生成工程后按 `{{androidPackageId}}` 修改(applicationId 不允许连字符,
8
+ * iOS bundle id 不允许下划线——两端无法共用一个 id,见 README)。
9
+ * - `webDir`:CLI `adep app build` 注入的已发布前端产物(`app/site/`,与桌面壳同位置)。
10
+ * `adep.permissions.json`(原生侧权限门控清单)由 CLI 构建时写入 `app/site/` 下,随
11
+ * `npx cap sync` 同步进两端原生资源(Android `assets/public/` / iOS `Public/`)。
12
+ * - `server`:加载本地静态产物(不连 dev server);在线运行时下云函数经 `platformUrl` 服务端执行。
13
+ */
14
+ const config: CapacitorConfig = {
15
+ appId: '{{iosBundleId}}',
16
+ appName: '{{appName}}',
17
+ webDir: 'app/site',
18
+ server: {
19
+ androidScheme: 'https',
20
+ },
21
+ }
22
+
23
+ export default config
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{projectSlug}}-mobile-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "{{appName}} —— AgentDeploy 移动应用(Capacitor 壳,在线运行时)",
6
+ "scripts": {
7
+ "cap:sync": "cap sync",
8
+ "cap:add:ios": "cap add ios",
9
+ "cap:add:android": "cap add android",
10
+ "build:ios": "adep app build --platform ios --dir .",
11
+ "build:android": "adep app build --platform android --dir .",
12
+ "open:ios": "cap open ios",
13
+ "open:android": "cap open android"
14
+ },
15
+ "dependencies": {
16
+ "@adep/bridge": "file:plugins/adep-bridge",
17
+ "@capacitor/core": "^7.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@capacitor/cli": "^7.0.0",
21
+ "@capacitor/ios": "^7.0.0",
22
+ "@capacitor/android": "^7.0.0"
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ android {
4
+ namespace "com.adep.bridge"
5
+ compileSdk 34
6
+ defaultConfig {
7
+ minSdk 22
8
+ }
9
+ compileOptions {
10
+ sourceCompatibility JavaVersion.VERSION_17
11
+ targetCompatibility JavaVersion.VERSION_17
12
+ }
13
+ }
14
+
15
+ repositories {
16
+ google()
17
+ mavenCentral()
18
+ }
19
+
20
+ dependencies {
21
+ implementation 'com.getcapacitor:android:7.0.0'
22
+ }
@@ -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
+ }