@adep/cli 0.1.17 → 0.2.1
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.
- package/README.md +96 -12
- package/dist/index.js +27216 -10522
- package/dist/vite.js +72 -13
- package/package.json +7 -5
- package/templates/app-desktop/README.md +51 -0
- package/templates/app-desktop/main.mjs +108 -0
- package/templates/app-desktop/offline-runtime.mjs +129 -0
- package/templates/app-desktop/package.json +49 -0
- package/templates/app-desktop/preload.cjs +45 -0
- package/templates/app-desktop/template.json +14 -0
- package/templates/app-desktop/window.mjs +53 -0
- package/templates/app-mobile/README.md +89 -0
- package/templates/app-mobile/adep.permissions.json +4 -0
- package/templates/app-mobile/bridge/gate.mjs +88 -0
- package/templates/app-mobile/capacitor.config.ts +23 -0
- package/templates/app-mobile/package.json +24 -0
- package/templates/app-mobile/plugins/adep-bridge/android/build.gradle +22 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/AndroidManifest.xml +9 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepBridge.kt +86 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepKeystore.kt +81 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepPermissionGate.kt +81 -0
- package/templates/app-mobile/plugins/adep-bridge/capacitor.plugin.json +10 -0
- package/templates/app-mobile/plugins/adep-bridge/dist/plugin.js +44 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/AdepBridge.podspec +14 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepBridge.swift +69 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepKeychain.swift +58 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepPermissionGate.swift +82 -0
- package/templates/app-mobile/plugins/adep-bridge/package.json +14 -0
- package/templates/app-mobile/template.json +16 -0
- package/templates/app-mobile/web/adep-bridge.js +56 -0
|
@@ -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
|
+
})()
|