@adep/cli 0.4.4 → 0.4.6

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 (46) hide show
  1. package/README.md +107 -11
  2. package/dist/index.js +33802 -27354
  3. package/dist/vite.js +247 -17
  4. package/dist/worker-entry.js +163 -1
  5. package/package.json +7 -6
  6. package/templates/app-desktop/README.md +67 -2
  7. package/templates/app-desktop/identity-store.mjs +309 -0
  8. package/templates/app-desktop/identity.mjs +517 -0
  9. package/templates/app-desktop/main.cjs +17 -0
  10. package/templates/app-desktop/main.mjs +218 -2
  11. package/templates/app-desktop/offline-runtime.mjs +72 -3
  12. package/templates/app-desktop/package.json +10 -6
  13. package/templates/app-desktop/recovery-ui.mjs +300 -0
  14. package/templates/app-desktop/sync-loop.mjs +137 -0
  15. package/templates/app-mobile/README.md +47 -9
  16. package/templates/app-mobile/package.json +2 -1
  17. package/templates/app-mobile/plugins/adep-bridge/Package.swift +5 -1
  18. package/templates/app-mobile/plugins/adep-bridge/android/build.gradle +34 -3
  19. package/templates/app-mobile/plugins/adep-bridge/android/src/main/AndroidManifest.xml +12 -5
  20. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepBridge.kt +198 -12
  21. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepIdentity.kt +342 -0
  22. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepKeystore.kt +56 -2
  23. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepSqlite.kt +506 -0
  24. package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepSqliteLogic.kt +235 -0
  25. package/templates/app-mobile/plugins/adep-bridge/dist/plugin.js +30 -0
  26. package/templates/app-mobile/plugins/adep-bridge/ios/AdepBridge.podspec +3 -0
  27. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepBridge.swift +221 -9
  28. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepIdentity.swift +500 -0
  29. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepKeychain.swift +69 -17
  30. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepPermissionGate.swift +25 -4
  31. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepShellConfig.swift +54 -0
  32. package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepSqlite.swift +614 -0
  33. package/templates/app-mobile/plugins/adep-bridge/package.json +10 -0
  34. package/templates/app-mobile/template.json +12 -3
  35. package/templates/app-mobile/web/adep-bridge.js +103 -9
  36. package/templates/app-mobile/web/offline-runtime.js +230 -0
  37. package/templates/app-mobile-xtool/AdepShell-Info.plist +24 -0
  38. package/templates/app-mobile-xtool/Package.swift +59 -0
  39. package/templates/app-mobile-xtool/README.md +81 -0
  40. package/templates/app-mobile-xtool/Sources/AdepShell/App.swift +36 -0
  41. package/templates/app-mobile-xtool/Sources/AdepShell/ShellViewController.swift +223 -0
  42. package/templates/app-mobile-xtool/adep.permissions.json +4 -0
  43. package/templates/app-mobile-xtool/app/site/.gitkeep +0 -0
  44. package/templates/app-mobile-xtool/template.json +15 -0
  45. package/templates/app-mobile-xtool/xtool.yml +35 -0
  46. package/templates/app-mobile/plugins/adep-bridge/capacitor.plugin.json +0 -10
@@ -0,0 +1,235 @@
1
+ package com.adep.bridge
2
+
3
+ import org.json.JSONArray
4
+ import org.json.JSONObject
5
+
6
+ /**
7
+ * 设备本地 SQLite 桥的**纯逻辑层**(Android,APP-023;PRD §2.11.3「设备本地 SQLite」行)。
8
+ *
9
+ * 与 iOS 侧 `AdepSqlite.swift` + `AdepBridge.swift` 的决策逻辑**逐字同源**(错误码 / 参数绑定
10
+ * 三态分流 / 类型映射 / 多语句拒绝 / 占位符计数 / 成功信封),但不依赖
11
+ * `android.database.sqlite.*`(framework)——本文件只 import 标准 `org.json`(纯 JVM 独立 JAR,
12
+ * Android 平台也内置同名包),因此 **kotlinc 行为级门可真编译真跑**(`app-android-sqlite.test.ts`,
13
+ * 等价 iOS 的 swiftc 同模块手法)。执行层 {@link AdepSqlite}(`android.database.sqlite.SQLiteDatabase`)
14
+ * 消费本文件的判定;framework 类型进不去行为级门,但**所有会漂移的决策都在这里、都能真跑钉住**。
15
+ *
16
+ * ## 与 iOS 的契约锚(跨实现一致性——错误码/形状/信封是锚,不可分叉)
17
+ *
18
+ * - 错误码:`SQLITE_OPEN_FAILED` / `SQLITE_MULTIPLE_STATEMENTS` / `SQLITE_PREPARE_FAILED` /
19
+ * `SQLITE_EXEC_FAILED` / `SQLITE_BIND_FAILED` / `SQLITE_COLUMN_FAILED` / `SQLITE_INVALID_PARAM`,
20
+ * 与 iOS `AdepSqlite.Code.*`、JS 面 `offline-runtime.js` 的 catch 分支逐字一致;
21
+ * - 能力名:`sqlite.run` / `sqlite.all` / `sqlite.get`,与 `adep.permissions.json` 声明、
22
+ * iOS `withSqlite(capability:)`、`@adep/runtime` `DeviceSqliteBridge` 三向一致;
23
+ * - 信封:成功带 `{ ok: true, <业务键> }`,失败带 `{ ok: false, code, message }`,与 iOS
24
+ * 逐字一致(页面侧运行时按 `envelope.ok !== true` 放行)。
25
+ */
26
+ object SqliteCode {
27
+ const val OPEN_FAILED = "SQLITE_OPEN_FAILED"
28
+ const val MULTIPLE_STATEMENTS = "SQLITE_MULTIPLE_STATEMENTS"
29
+ const val PREPARE_FAILED = "SQLITE_PREPARE_FAILED"
30
+ const val EXEC_FAILED = "SQLITE_EXEC_FAILED"
31
+ const val BIND_FAILED = "SQLITE_BIND_FAILED"
32
+ const val COLUMN_FAILED = "SQLITE_COLUMN_FAILED"
33
+ const val INVALID_PARAM = "SQLITE_INVALID_PARAM"
34
+ }
35
+
36
+ /** 一条可读的 SQLite 执行错误(code + message;message 前缀是 JS 侧 catch 的锚,与 iOS 同形)。 */
37
+ class SqliteBridgeError(val code: String, override val message: String) : Exception(message)
38
+
39
+ /**
40
+ * 参数绑定值分流(三态,2026-10-22 iOS 真机第五轮实证——混并会导致 CAPPluginCall 悬空、
41
+ * JS promise 永不落定、页面装配**静默卡死**,比报错毒)。Capacitor 的 JS 值落地 JSON:
42
+ * String / 数值 / Boolean / `JSONObject.NULL`:
43
+ * - 键缺席 / null → `[]`(无绑定参数,合法——DDL / 无参 DML 走这);
44
+ * - 数组内元素非法(对象 / 嵌套数组)→ 抛 `SqliteBridgeError(INVALID_PARAM, 点名下标)`;
45
+ * - 合法元素 → 逐个映射,null 用 `JSONObject.NULL` 表示(执行层直接落 NULL)。
46
+ *
47
+ * @param raw 页面侧 `params` 的 JSON 数组;可为空(无参)。
48
+ * @return 绑定值列表,与 SQL 占位符一一对应(顺序即占位符顺序)。
49
+ */
50
+ fun sqliteBindParams(raw: JSONArray?): List<Any> {
51
+ if (raw == null) return emptyList()
52
+ val list = ArrayList<Any>(raw.length())
53
+ for (i in 0 until raw.length()) {
54
+ val item = raw.opt(i)
55
+ when {
56
+ item == null || item === JSONObject.NULL -> list.add(JSONObject.NULL)
57
+ item is String -> list.add(item)
58
+ item is Number -> list.add(item)
59
+ item is Boolean -> list.add(item)
60
+ else -> throw SqliteBridgeError(
61
+ SqliteCode.INVALID_PARAM,
62
+ "params[$i] 类型不支持(支持 string / number / boolean / null)",
63
+ )
64
+ }
65
+ }
66
+ return list
67
+ }
68
+
69
+ /**
70
+ * SQL 文本的**多语句判定**:统计「有效语句数」。iOS 侧是 `sqlite3_prepare_v2` 的 pzTail 判定
71
+ * (尾随非空即拒);Android 侧 `SQLiteDatabase` 的 `execSQL` 也只执行第一条并**静默丢弃尾随**
72
+ * (同族静默陷阱)⇒ 在 Android 侧**先把多语句掐掉**,语义与 iOS 一致:引号内(`'...'`/`"..."`)
73
+ * 与注释内(`--` / `/* */`)的分号**不是**语句边界,末尾分号是终结符(不增段),纯注释/空白
74
+ * 文本 → 0。
75
+ *
76
+ * 口径:**分号分隔的「非空段」数**。末尾分号之后若只剩空白/注释,则它是终结符不产生新段。
77
+ *
78
+ * @return 有效语句数;空文本(全注释/空白)返回 0,单语句(含末分号 / 无分号)返回 1,多条 → >1。
79
+ */
80
+ fun countSqlStatements(sql: String): Int {
81
+ var count = 0
82
+ var i = 0
83
+ var inSingle = false
84
+ var inDouble = false
85
+ var inLineComment = false
86
+ var inBlockComment = false
87
+ var inStatement = false // 当前是否在一条「有实质内容」的语句内(剥注释/空白)
88
+ val n = sql.length
89
+ while (i < n) {
90
+ val c = sql[i]
91
+ val next = if (i + 1 < n) sql[i + 1] else '\u0000'
92
+ if (inSingle) {
93
+ if (c == '\'') inSingle = false
94
+ inStatement = true
95
+ i++
96
+ continue
97
+ }
98
+ if (inDouble) {
99
+ if (c == '"') inDouble = false
100
+ inStatement = true
101
+ i++
102
+ continue
103
+ }
104
+ if (inLineComment) {
105
+ if (c == '\n') {
106
+ inLineComment = false
107
+ // 换行不算语句内容(剥),但也不终结语句。
108
+ }
109
+ i++
110
+ continue
111
+ }
112
+ if (inBlockComment) {
113
+ if (c == '*' && next == '/') {
114
+ inBlockComment = false
115
+ i++ // 跳过 `*`(当前 i 指向 *);分支末尾的 i++ 会再跳过 `/`,使 / 不残留
116
+ }
117
+ i++
118
+ continue
119
+ }
120
+ when {
121
+ c == '\'' -> { inSingle = true; inStatement = true; i++ }
122
+ c == '"' -> { inDouble = true; inStatement = true; i++ }
123
+ c == '-' && next == '-' -> { inLineComment = true; i++ }
124
+ c == '/' && next == '*' -> { inBlockComment = true; i++ }
125
+ c == ';' -> {
126
+ if (inStatement) { count++ ; inStatement = false }
127
+ i++
128
+ }
129
+ c.isWhitespace() -> i++
130
+ else -> { inStatement = true; i++ }
131
+ }
132
+ }
133
+ if (inStatement) count++
134
+ return count
135
+ }
136
+
137
+ /**
138
+ * 占位符计数判定:JS 侧绑定值数必须**恰好等于** SQL 占位符数。
139
+ * ⚠️ SQLite 对「多了」静默忽略(SQLITE_RANGE),对「少了」把缺的占位符按 NULL 执行——
140
+ * 两者都不报错,积的是「不报错只恒不命中」的静默陷阱(iOS 行为探针 case12 实证:INSERT
141
+ * 两列只给一列 ⇒ 行照插、另一列 NULL)。两侧都 fail loud。
142
+ *
143
+ * @return 独立占位符个数(`?` / `?N` / `:name` 各算一个;同名命名占位符算一个)。
144
+ */
145
+ fun countPlaceholders(sql: String): Int {
146
+ var count = 0
147
+ var i = 0
148
+ var inSingle = false
149
+ var inDouble = false
150
+ var inLineComment = false
151
+ var inBlockComment = false
152
+ val named = HashSet<String>()
153
+ val n = sql.length
154
+ while (i < n) {
155
+ val c = sql[i]
156
+ val next = if (i + 1 < n) sql[i + 1] else '\u0000'
157
+ if (inSingle) {
158
+ if (c == '\'') inSingle = false
159
+ i++
160
+ continue
161
+ }
162
+ if (inDouble) {
163
+ if (c == '"') inDouble = false
164
+ i++
165
+ continue
166
+ }
167
+ if (inLineComment) {
168
+ if (c == '\n') inLineComment = false
169
+ i++
170
+ continue
171
+ }
172
+ if (inBlockComment) {
173
+ if (c == '*' && next == '/') inBlockComment = false
174
+ i++
175
+ continue
176
+ }
177
+ when {
178
+ c == '-' && next == '-' -> { inLineComment = true; i++ }
179
+ c == '/' && next == '*' -> { inBlockComment = true; i++ }
180
+ c == '\'' -> inSingle = true
181
+ c == '"' -> inDouble = true
182
+ c == '?' -> {
183
+ // `?` / `?NNN` 整体算一个匿名占位符。
184
+ while (i + 1 < n && sql[i + 1].isDigit()) i++
185
+ count++
186
+ }
187
+ c == ':' || c == '@' || c == '$' -> {
188
+ // `:name` / `@name` / `$name` 命名占位符:同名去重,不同名各算一个。
189
+ var j = i + 1
190
+ while (j < n && (sql[j].isLetterOrDigit() || sql[j] == '_')) j++
191
+ if (j > i + 1) {
192
+ val name = sql.substring(i, j)
193
+ if (named.add(name)) count++
194
+ i = j - 1
195
+ } else {
196
+ count++
197
+ }
198
+ }
199
+ }
200
+ i++
201
+ }
202
+ return count
203
+ }
204
+
205
+ /**
206
+ * 类型映射:SQLite 存储类型值 → JS 侧类型标签。
207
+ *
208
+ * Android `Cursor.getType(column)` 返回 `Cursor.FIELD_TYPE_*`(INTEGER 1 / FLOAT 2 / STRING 3 /
209
+ * BLOB 4 / NULL 0)。映射为标签供执行层把 `Cursor` 读成 `JSONObject` 时用。⚠️ 超 2^53 精度:
210
+ * 设备端 owned 表 id 是 snowflake 的场景 id 一律字符串(SHOP-007 纪律),JS 侧由调用方保证。
211
+ *
212
+ * @param fieldType `Cursor.getType()` 的原始值(0..4)。
213
+ * @return 标签;未知类型 → null(调用方 fail loud)。
214
+ */
215
+ fun mapCursorFieldType(fieldType: Int): String? = when (fieldType) {
216
+ 1 -> "integer"
217
+ 2 -> "float"
218
+ 3 -> "string"
219
+ 4 -> "blob"
220
+ 0 -> "null"
221
+ else -> null
222
+ }
223
+
224
+ /**
225
+ * 成功信封(带 `ok: true`)的组装——**唯一**组装点(与 iOS `sqliteSuccessEnvelope` 同契约)。
226
+ * ⚠️ 必须带 `ok: true`:页面侧运行时(offline-runtime.js)放行条件是 `envelope.ok !== true`
227
+ * 即抛;成功信封漏 ok ⇒ 真机 `INVOKE ERR [undefined]`(iOS 2026-10-22 实测)。失败信封在
228
+ * 调用方直接带 `ok:false`(见 {@link AdepBridge}),本函数只管成功面。
229
+ *
230
+ * @param data 业务键(run → changes / all → rows / get → row),不得含 `ok`(会覆盖为 true)。
231
+ */
232
+ fun sqliteSuccessEnvelope(data: JSONObject): JSONObject {
233
+ data.put("ok", true)
234
+ return data
235
+ }
@@ -38,6 +38,36 @@ const AdepBridge = registerPlugin('AdepBridge', {
38
38
  value: null,
39
39
  reason: 'NATIVE_UNAVAILABLE',
40
40
  }),
41
+ // 读恢复码(APP-034):页面用它呈现 / 导出认领凭据(服务端只存摘要、无法重发)。
42
+ // 只读——写凭据的通道仍在原生侧(页面拿不到,XSS 不能替换 ownerKey 劫持分区)。
43
+ getRecoveryCode: () =>
44
+ Promise.resolve({
45
+ value: null,
46
+ reason: 'NATIVE_UNAVAILABLE',
47
+ }),
48
+ // —— 设备本地 SQLite(APP-022 B 段)——
49
+ // 三个方法与 AdepBridge.swift 的 pluginMethods **逐字**对齐(adep-bridge-spm.test.ts
50
+ // 的集合相等门咬着:多写少写都静默)。Web fallback 一律 NATIVE_UNAVAILABLE(fail-closed,
51
+ // 与门控方向一致);原生成功信封 { ok: true, changes } / { ok: true, rows } /
52
+ // { ok: true, row }——ok 语义与 invokeNative 一致(页面侧运行时按 envelope.ok 放行)。
53
+ sqliteRun: () =>
54
+ Promise.resolve({
55
+ ok: false,
56
+ code: 'NATIVE_UNAVAILABLE',
57
+ message: '设备本地 SQLite 在 Web 环境不可用(移动壳原生侧才提供)',
58
+ }),
59
+ sqliteAll: () =>
60
+ Promise.resolve({
61
+ ok: false,
62
+ code: 'NATIVE_UNAVAILABLE',
63
+ message: '设备本地 SQLite 在 Web 环境不可用(移动壳原生侧才提供)',
64
+ }),
65
+ sqliteGet: () =>
66
+ Promise.resolve({
67
+ ok: false,
68
+ code: 'NATIVE_UNAVAILABLE',
69
+ message: '设备本地 SQLite 在 Web 环境不可用(移动壳原生侧才提供)',
70
+ }),
41
71
  })
42
72
 
43
73
  export { AdepBridge }
@@ -14,4 +14,7 @@ Pod::Spec.new do |s|
14
14
  s.source_files = 'Sources/**/*.swift'
15
15
  s.swift_version = '5.1'
16
16
  s.dependency 'Capacitor'
17
+ # APP-025:SQLCipher 加密(与 SPM 形态的 SQLCipher.swift 4.19 同源同版语义;
18
+ # CocoaPods 官方 SQLCipher pod 是源码编译形态,版本号与 SQLCipher.swift release 对齐)。
19
+ s.dependency 'SQLCipher', '~> 4.19'
17
20
  end
@@ -1,5 +1,44 @@
1
1
  import Capacitor
2
2
 
3
+ /// sqlite 成功信封的**唯一组装点**(纯函数,Foundation-only ⇒ 行为级门可真跑——
4
+ /// `adep-bridge-envelope.test.ts` 与 `AdepSqlite` 同手法编译本函数验证信封形状)。
5
+ ///
6
+ /// ⚠️ **必须带 `ok: true`**:页面侧运行时(offline-runtime.js)的放行条件是
7
+ /// `envelope.ok !== true` 即抛。2026-10-22 真机实测:成功信封漏 ok ⇒ 函数一跑就
8
+ /// `INVOKE ERR [undefined]`(ok / code 双 undefined)。失败信封一直带 ok/code,
9
+ /// 唯独成功信封漏了——跨层契约只有行为级真跑能钉住(静态断言看不见「缺键」)。
10
+ func sqliteSuccessEnvelope(_ data: [String: Any]) -> [String: Any] {
11
+ var envelope = data
12
+ envelope["ok"] = true
13
+ return envelope
14
+ }
15
+
16
+ /// params 绑定错误(调用方按 SQLITE_INVALID_PARAM 回信封——与执行期 SQLITE_ERROR 分流)。
17
+ struct SqliteParamError: Error {
18
+ let message: String
19
+ }
20
+
21
+ /// params 元素 → SQLite 绑定值(纯函数,Foundation-only ⇒ 行为级门可真跑,与
22
+ /// `sqliteSuccessEnvelope` 同手法)。Capacitor 的 JS 值落地即 String / Double / Bool /
23
+ /// NSNull;其他类型(对象 / 嵌套数组 / 函数)不在 SQLite 标量绑定面,静默转字符串会
24
+ /// 造出「查不出为什么等于不上」的行 ⇒ 抛错(点名下标,不静默)。
25
+ func sqliteBindParams(_ raw: [Any]) throws -> [Any] {
26
+ var list: [Any] = []
27
+ for (index, item) in raw.enumerated() {
28
+ switch item {
29
+ case let text as String: list.append(text)
30
+ case let number as Double: list.append(number)
31
+ case let flag as Bool: list.append(flag)
32
+ case is NSNull: list.append(NSNull())
33
+ default:
34
+ throw SqliteParamError(
35
+ message: "params[\(index)] 类型不支持(支持 string / number / boolean / null)"
36
+ )
37
+ }
38
+ }
39
+ return list
40
+ }
41
+
3
42
  /// AgentDeploy 移动壳原生桥(APP-009,iOS 侧)。
4
43
  ///
5
44
  /// 职责(与 bridge/gate.mjs 的 host-side 镜像同规则;Android 侧 AdepBridge.kt 同形):
@@ -12,11 +51,55 @@ import Capacitor
12
51
  ///
13
52
  /// 权限清单来源:CLI `adep app build` 把 `adep.permissions.json` 写入 `app/site/`;
14
53
  /// `npx cap sync` 把它同步进 iOS 工程的 `Public/`(folder reference),运行时经
15
- /// `Bundle.main` 读取。
54
+ /// `Bundle.main` 读取(候选路径见 `AdepPermissionGate.manifestCandidateSubdirectories`)。
55
+ ///
56
+ /// ## 为什么要 conform `CAPBridgedPlugin`(APP-018)
57
+ ///
58
+ /// Capacitor 7 的注册路径对插件有**强制**一致性要求(`internal typealias CapacitorPlugin =
59
+ /// CAPPlugin & CAPBridgedPlugin`):
60
+ /// - `loadPlugin(type:)` 里 `type.init() as? CapacitorPlugin` 失败 → **只打一行日志**、返回 nil;
61
+ /// - `registerPluginInstance(_:)` 同样 `as? CapacitorPlugin`,失败 → 同样只打日志;
62
+ /// - 而 `registerPluginType(_:)` 首行是 `if autoRegisterPlugins { return }`,`CAPBridgeViewController`
63
+ /// 构造 bridge 时没传该开关(默认 true)⇒ **类型注册是空操作**。
64
+ ///
65
+ /// 三者都不是编译错误、也不是抛异常:症状是 JS 侧回落到 `dist/plugin.js` 的
66
+ /// `NATIVE_UNAVAILABLE` 桩——「一切都不可用」,而不是「插件没注册」。所以一致性声明是硬要求,
67
+ /// 且必须与 JS 面**逐字对齐**(`identifier` / `jsName` / `pluginMethods`,见 `adep-bridge-spm.test.ts`)。
16
68
  @objc(AdepBridge)
17
- public class AdepBridge: CAPPlugin {
69
+ public class AdepBridge: CAPPlugin, CAPBridgedPlugin {
70
+ /// 原生侧插件标识;与 `jsName` 同值(Capacitor 用 `jsName` 做注册表的键)。
71
+ public let identifier = "AdepBridge"
72
+ /// JS 侧注册名:必须与 `dist/plugin.js` 的 `registerPlugin('AdepBridge', …)` 首个参数逐字相等,
73
+ /// 否则页面永远找不到原生实现(且不报错)。
74
+ public let jsName = "AdepBridge"
75
+
76
+ /// 暴露给 JS 的方法清单。
77
+ ///
78
+ /// ⚠️ 这份名单**只列 JS 面声明过的方法**(`dist/plugin.js` 的对象键),且必须与它**相等**:
79
+ /// 多列一个 = JS 面永远调不到的注册项;少列一个 = 页面静默回落 NATIVE_UNAVAILABLE 桩。
80
+ /// 写凭据的入口不在 JS 面(APP-027:ownerKey 由原生自举签发,页面只能读)。
81
+ public let pluginMethods: [CAPPluginMethod] = [
82
+ CAPPluginMethod(name: "invokeNative", returnType: CAPPluginReturnPromise),
83
+ CAPPluginMethod(name: "getOwnerKey", returnType: CAPPluginReturnPromise),
84
+ // APP-034:读恢复码(只读;写口仍在原生侧)。与 dist/plugin.js 对象键逐字对齐。
85
+ CAPPluginMethod(name: "getRecoveryCode", returnType: CAPPluginReturnPromise),
86
+ // APP-022 B 段:设备本地 SQLite(PRD §2.11.3)。三个方法与 dist/plugin.js 的
87
+ // 对象键**逐字**对齐(adep-bridge-spm.test.ts 的集合相等门咬着);门控与
88
+ // invokeNative 同一条路:未在 adep.permissions.json 声明即 PERMISSION_DENIED。
89
+ CAPPluginMethod(name: "sqliteRun", returnType: CAPPluginReturnPromise),
90
+ CAPPluginMethod(name: "sqliteAll", returnType: CAPPluginReturnPromise),
91
+ CAPPluginMethod(name: "sqliteGet", returnType: CAPPluginReturnPromise),
92
+ ]
93
+
18
94
  private var gate: AdepPermissionGate?
19
95
 
96
+ /// 设备本地 SQLite(APP-022 B 段)。懒建:首次 sqlite 调用时初始化
97
+ /// (Application Support 目录在首启前可能尚未就绪;懒加载避免 load() 期做 IO)。
98
+ private var sqlite: AdepSqlite?
99
+
100
+ /// 安装身份自举(APP-027)。持有引用只为防提前释放(自举在后台队列上跑)。
101
+ private var identity: AdepIdentityBootstrap?
102
+
20
103
  public override func load() {
21
104
  super.load()
22
105
  do {
@@ -27,6 +110,47 @@ public class AdepBridge: CAPPlugin {
27
110
  gate = nil
28
111
  CAPLog.print("[adep-bridge] 权限清单加载失败:\(error)")
29
112
  }
113
+ // APP-025:库 key = Keychain 的 ownerKey(§2.11.5 设备身份与库加密同源——拿到
114
+ // 设备身份即拿到库钥匙;卸载重装后 Keychain 条目仍在,库仍可读)。静态钩子在
115
+ // AdepSqlite(被行为级门单文件编译,不拖 Security framework),这里只接线。
116
+ AdepSqlite.defaultKeyProvider = { AdepKeychain.loadOwnerKey() }
117
+ let database = AdepSqlite()
118
+ sqlite = database
119
+ startIdentityBootstrap(database: database)
120
+ }
121
+
122
+ /// 安装身份自举(APP-027):首启向服务端签发端点换取 ownerKey,只存 Keychain
123
+ /// (APP-028:恢复码同批落库——服务端只存摘要,明文只在这次响应里存在)。
124
+ ///
125
+ /// 三条边界(与 `AdepIdentityBootstrap` 的注释同口径):
126
+ /// 1. **后台队列**——`load()` 在 `viewDidLoad` 链上,同步网络会阻塞首屏;
127
+ /// 2. **失败不阻断**——断网 / 5xx / 响应形态不符只记带原因码的日志,下次启动重试;
128
+ /// 3. **幂等**——已有 ownerKey 不发请求(行为级门的分母断言:请求数 == 1)。
129
+ ///
130
+ /// ⚠️ **不做在 JS 面**:页面拿不到写凭据的通道(XSS 不能替换 ownerKey 劫持分区),
131
+ /// `dist/plugin.js` 的对象键集合与 `pluginMethods` 名单都不变。
132
+ private func startIdentityBootstrap(database: AdepSqlite) {
133
+ let bootstrap = AdepIdentityBootstrap(
134
+ environment: AdepIdentityBootstrap.Environment(
135
+ configData: { AdepShellConfig.data(named: AdepShellConfig.configFileName, in: Bundle.main) },
136
+ storedOwnerKey: { AdepKeychain.loadOwnerKey() },
137
+ persistOwnerKey: { AdepKeychain.saveOwnerKey($0) },
138
+ // APP-028:恢复码与 ownerKey 一起落 Keychain(服务端只存摘要,明文只此一次)。
139
+ // 读口在此的唯一用处是**写后读回校验**——写成功读不出等于码已丢。
140
+ storedRecoveryCode: { AdepKeychain.loadRecoveryCode() },
141
+ persistRecoveryCode: { AdepKeychain.saveRecoveryCode($0) },
142
+ transport: AdepURLSessionTransport(),
143
+ platform: "ios",
144
+ // NSLog 而非 CAPLog.print:SpringBoard 拉起的 app 没人接 stdout(见 DiagMessageHandler)。
145
+ log: { NSLog("[adep-identity] \($0)") },
146
+ // 库已有数据且此前用 fallbackKey 建的 ⇒ 这里换成正式 key(rekey 在 AdepSqlite 内收口)。
147
+ onIssued: { key in database.adoptKey(key) }
148
+ )
149
+ )
150
+ identity = bootstrap
151
+ DispatchQueue.global(qos: .utility).async {
152
+ bootstrap.start()
153
+ }
30
154
  }
31
155
 
32
156
  /// 调用原生能力。门控先于一切实现:未声明 → 拒绝 + 审计;已声明 → 按能力分发。
@@ -51,19 +175,107 @@ public class AdepBridge: CAPPlugin {
51
175
  }
52
176
  }
53
177
 
54
- /// 读 ownerKey(Keychain)。无则返回 null(首启未签发)。
178
+ /// 读 ownerKey(Keychain)。首启尚未签发(或签发失败)时返回 null。
179
+ ///
180
+ /// ⚠️ **`setOwnerKey` 已删除**(APP-027):它曾是「页面调不到、原生也没人调」的死方法——
181
+ /// 写凭据的入口现在只有 `AdepIdentityBootstrap`(首启自举)与 `AdepKeychain.saveOwnerKey`
182
+ /// (恢复码换键流程的前置),两者都在原生侧,页面永远拿不到写凭据的通道。
55
183
  @objc func getOwnerKey(_ call: CAPPluginCall) {
56
184
  let value = AdepKeychain.loadOwnerKey()
57
185
  call.resolve(["value": value ?? NSNull()])
58
186
  }
59
187
 
60
- /// 写 ownerKey(Keychain)。安装首启时由壳原生侧调用服务端签发端点后存入。
61
- @objc func setOwnerKey(_ call: CAPPluginCall) {
62
- guard let key = call.getString("key"), !key.isEmpty else {
63
- call.reject("key 必须为非空字符串")
188
+ /// 读恢复码(APP-034,Keychain `adep.recoveryCode`)。无则 null(未签发 / 读失败)。
189
+ ///
190
+ /// ⚠️ **只读**:恢复码的写口在 `AdepIdentityBootstrap`(首启自举),页面没有写凭据的
191
+ /// 通道。页面拿它做呈现 / 导出(认领凭据——服务端只存摘要、无法重发,丢了等于放弃
192
+ /// 认领);一次性弹层的节奏由页面产品代码决定,壳只保证「读得到」。
193
+ @objc func getRecoveryCode(_ call: CAPPluginCall) {
194
+ let value = AdepKeychain.loadRecoveryCode()
195
+ call.resolve(["value": value ?? NSNull()])
196
+ }
197
+
198
+ // MARK: - 设备本地 SQLite(APP-022 B 段)
199
+
200
+ /// 把 CAPPluginCall 的 `params`(JS 数组)转成 AdepSqlite 的绑定面 `[Any]`。
201
+ ///
202
+ /// ⚠️ 三态必须分流(2026-10-22 真机第五轮实测:缺席与「数组内元素非法」共用 nil
203
+ /// 返回 ⇒ `withSqlite` 的 guard-else 以为「错误信封已 resolve」直接 return,而缺席
204
+ /// 路径什么都没 resolve ⇒ CAPPluginCall 悬空,JS 侧 promise 永不落定,页面装配
205
+ /// **静默卡死**——比报错毒得多,浮层连 ERR 行都没有):
206
+ /// - 键缺席 / JS 传 null → 返回 `[]`(无绑定参数,合法——DDL/无参 DML 走这);
207
+ /// - 数组内元素非法 → 抛 `SqliteParamError`(调用方回 SQLITE_INVALID_PARAM);
208
+ /// - 合法元素 → 逐个映射。
209
+ private func convertParams(_ call: CAPPluginCall) throws -> [Any] {
210
+ guard let raw = call.getArray("params") else { return [] }
211
+ return try sqliteBindParams(raw)
212
+ }
213
+
214
+ /// sqlite 三方法的公共骨架:门控 → 取参 → 执行 → 信封返回。
215
+ /// 形状对齐 `DeviceSqliteBridge`(bridge.ts):sql + params(可选数组);
216
+ /// run → { changes },all → { rows },get → { row | null }。
217
+ private func withSqlite(
218
+ _ call: CAPPluginCall,
219
+ capability: String,
220
+ _ body: (AdepSqlite, String, [Any]) throws -> [String: Any]
221
+ ) {
222
+ guard let gate = gate else {
223
+ call.resolve(["ok": false, "code": "GATE_UNAVAILABLE", "message": "权限门控未建立(清单缺失)"])
224
+ return
225
+ }
226
+ guard case .granted = gate.check(capability) else {
227
+ // 门控拒绝的 reason 已进审计;这里只做信封。code 用 PERMISSION_DENIED(契约常量)。
228
+ call.resolve([
229
+ "ok": false, "code": AdepPermissionGate.permissionDenied,
230
+ "message": "原生能力 \(capability) 未在应用 manifest 的 permissions 中声明",
231
+ ])
64
232
  return
65
233
  }
66
- AdepKeychain.saveOwnerKey(key)
67
- call.resolve()
234
+ guard let sql = call.getString("sql"), !sql.isEmpty else {
235
+ call.resolve(["ok": false, "code": "SQLITE_INVALID_PARAM", "message": "sql 必须为非空字符串"])
236
+ return
237
+ }
238
+ guard let sqlite = sqlite else {
239
+ call.resolve(["ok": false, "code": "SQLITE_OPEN_FAILED", "message": "SQLite 未初始化"])
240
+ return
241
+ }
242
+ do {
243
+ // params 缺席 → [](合法无参),元素非法 → 抛错回 SQLITE_INVALID_PARAM——
244
+ // 三态分流见 convertParams 注释;每条路径都恰好 resolve 一次。
245
+ let bindParams = try convertParams(call)
246
+ let data = try body(sqlite, sql, bindParams)
247
+ // 成功信封统一经唯一组装点(ok: true 契约的注释与门见函数声明处)。
248
+ call.resolve(sqliteSuccessEnvelope(data))
249
+ } catch let error as SqliteParamError {
250
+ call.resolve(["ok": false, "code": "SQLITE_INVALID_PARAM", "message": error.message])
251
+ } catch {
252
+ // fail loud(AdepSqlite 的错误 message 已带 code 前缀与 sql 片段);
253
+ // resolve 而非 reject:JS 侧统一按 { ok, code, message } 信封分支。
254
+ call.resolve(["ok": false, "code": "SQLITE_ERROR", "message": error.localizedDescription])
255
+ }
256
+ }
257
+
258
+ /// 执行写语句(DDL / DML)。JS 面:`AdepBridge.sqliteRun({ sql, params? })` → `{ changes }`。
259
+ @objc func sqliteRun(_ call: CAPPluginCall) {
260
+ withSqlite(call, capability: "sqlite.run") { sqlite, sql, params in
261
+ let changes = try sqlite.run(sql, params: params.isEmpty ? nil : params)
262
+ return ["changes": changes]
263
+ }
264
+ }
265
+
266
+ /// 执行查询返回多行。JS 面:`AdepBridge.sqliteAll({ sql, params? })` → `{ rows }`。
267
+ @objc func sqliteAll(_ call: CAPPluginCall) {
268
+ withSqlite(call, capability: "sqlite.all") { sqlite, sql, params in
269
+ let rows = try sqlite.all(sql, params: params.isEmpty ? nil : params)
270
+ return ["rows": rows]
271
+ }
272
+ }
273
+
274
+ /// 执行单行查询。JS 面:`AdepBridge.sqliteGet({ sql, params? })` → `{ row | null }`。
275
+ @objc func sqliteGet(_ call: CAPPluginCall) {
276
+ withSqlite(call, capability: "sqlite.get") { sqlite, sql, params in
277
+ let row = try sqlite.get(sql, params: params.isEmpty ? nil : params)
278
+ return ["row": row ?? NSNull()]
279
+ }
68
280
  }
69
281
  }