@clowder-ai/personal-chrome-companion 0.0.0-stage → 0.1.0-alpha.4
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.en.md +129 -0
- package/README.md +33 -2
- package/assets/icon.svg +3 -0
- package/dist/assistant-return-cursor.d.ts +13 -0
- package/dist/assistant-return-cursor.d.ts.map +1 -0
- package/dist/assistant-return-cursor.js +37 -0
- package/dist/assistant-return-cursor.js.map +1 -0
- package/dist/conversation-host.d.ts +33 -0
- package/dist/conversation-host.d.ts.map +1 -0
- package/dist/conversation-host.js +237 -0
- package/dist/conversation-host.js.map +1 -0
- package/dist/extension-delivery.d.ts +6 -0
- package/dist/extension-delivery.d.ts.map +1 -0
- package/dist/extension-delivery.js +71 -0
- package/dist/extension-delivery.js.map +1 -0
- package/dist/helper-reachability.d.ts +35 -0
- package/dist/helper-reachability.d.ts.map +1 -0
- package/dist/helper-reachability.js +76 -0
- package/dist/helper-reachability.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/pairing-record.d.ts +12 -0
- package/dist/pairing-record.d.ts.map +1 -0
- package/dist/pairing-record.js +86 -0
- package/dist/pairing-record.js.map +1 -0
- package/dist/personal-chrome-host-transport.d.ts +66 -0
- package/dist/personal-chrome-host-transport.d.ts.map +1 -0
- package/dist/personal-chrome-host-transport.js +296 -0
- package/dist/personal-chrome-host-transport.js.map +1 -0
- package/dist/plugin-entrypoint.d.ts +61 -0
- package/dist/plugin-entrypoint.d.ts.map +1 -0
- package/dist/plugin-entrypoint.js +217 -0
- package/dist/plugin-entrypoint.js.map +1 -0
- package/dist/protocol.d.ts +110 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +257 -0
- package/dist/protocol.js.map +1 -0
- package/dist/runtime-delivery.d.ts +18 -0
- package/dist/runtime-delivery.d.ts.map +1 -0
- package/dist/runtime-delivery.js +147 -0
- package/dist/runtime-delivery.js.map +1 -0
- package/dist/title-refresh-protocol.d.ts +17 -0
- package/dist/title-refresh-protocol.d.ts.map +1 -0
- package/dist/title-refresh-protocol.js +26 -0
- package/dist/title-refresh-protocol.js.map +1 -0
- package/extension/chatgpt-composer-transaction.mjs +100 -0
- package/extension/chatgpt-page-adapter.mjs +625 -0
- package/extension/chatgpt-page-contract.mjs +188 -0
- package/extension/content-script-entry.mjs +139 -0
- package/extension/content-script.js +7 -0
- package/extension/icons/gpt-pro-128.png +0 -0
- package/extension/icons/gpt-pro-16.png +0 -0
- package/extension/icons/gpt-pro-32.png +0 -0
- package/extension/icons/gpt-pro-48.png +0 -0
- package/extension/manifest.json +34 -0
- package/extension/service-worker.js +349 -0
- package/native-host/assistant-return-inbox.mjs +294 -0
- package/native-host/conversation-binding.d.mts +60 -0
- package/native-host/conversation-binding.mjs +342 -0
- package/native-host/conversation-title-exchange.mjs +136 -0
- package/native-host/conversation-titles.d.mts +27 -0
- package/native-host/conversation-titles.mjs +113 -0
- package/native-host/install-host.d.mts +63 -0
- package/native-host/install-host.mjs +375 -0
- package/native-host/native-framing.mjs +39 -0
- package/native-host/native-host-artifact.d.mts +1 -0
- package/native-host/native-host-artifact.mjs +127 -0
- package/native-host/native-host-cli.mjs +8 -0
- package/native-host/native-host-install-contract.mjs +135 -0
- package/native-host/native-host-install-files.mjs +65 -0
- package/native-host/native-host.mjs +816 -0
- package/native-host/native-ledger.mjs +95 -0
- package/native-host/native-results.mjs +200 -0
- package/native-host/native-socket-lease.d.mts +1 -0
- package/native-host/native-socket-lease.mjs +183 -0
- package/native-host/pairing-record.mjs +147 -0
- package/native-host/setup-host.mjs +186 -0
- package/native-host/setup-protocol.md +74 -0
- package/npm-shrinkwrap.json +120 -0
- package/package.json +65 -4
- package/plugin.yaml +70 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Personal Chrome companion
|
|
2
|
+
|
|
3
|
+
[Chinese](README.md) · English
|
|
4
|
+
|
|
5
|
+
`@clowder-ai/personal-chrome-companion` connects explicitly authorized ChatGPT conversations through a Chrome MV3 extension and a local Native Messaging helper.
|
|
6
|
+
|
|
7
|
+
The Host-loadable builtin runtime is implemented in this package:
|
|
8
|
+
`runtime.transport: builtin` with entrypoint `dist/plugin-entrypoint.js`. The
|
|
9
|
+
plugin declares `runtime.dataDirectory: personal-chrome-host`, so the Host
|
|
10
|
+
provisions `<projectRoot>/.cat-cafe/plugin-host/personal-chrome-host` (0700)
|
|
11
|
+
for this installation's private data.
|
|
12
|
+
|
|
13
|
+
## Module plugin surface
|
|
14
|
+
|
|
15
|
+
Conversation access requires your explicit authorization. Registering the extension with the operating
|
|
16
|
+
system is a separate setup step; installing the plugin alone does neither.
|
|
17
|
+
|
|
18
|
+
The generic installed setup and real-account journey require separate acceptance; a package archive test is not that evidence.
|
|
19
|
+
|
|
20
|
+
The `personal-chrome-host` feature owns one `cloud-conversation-host`
|
|
21
|
+
contribution (`provider: chatgpt`). Its three conversation methods use the
|
|
22
|
+
package's native-host socket client and the helper's v2 protocol:
|
|
23
|
+
|
|
24
|
+
- `personal-chrome-host.append-message` sends to an authorized conversation.
|
|
25
|
+
- `personal-chrome-host.assistant-returns.list` fetches at most one pending reply.
|
|
26
|
+
- `personal-chrome-host.assistant-returns.ack` acknowledges that reply; listing
|
|
27
|
+
alone never removes it.
|
|
28
|
+
|
|
29
|
+
The `personalChromeAuthorizations` operation lists the authorized
|
|
30
|
+
conversations as Host-rendered rows (one `revoke` row action each), revokes one
|
|
31
|
+
authorization by `conversationId`, and reports authorization status. The
|
|
32
|
+
underlying store holds at most 32 authorizations; the operation surfaces that
|
|
33
|
+
ceiling through the status action.
|
|
34
|
+
|
|
35
|
+
Use **Refresh titles** to ask the connected extension for readable titles of
|
|
36
|
+
the currently authorized conversations. The result reports how many titles
|
|
37
|
+
were updated, or why refreshing was unavailable; the conversation list then
|
|
38
|
+
reloads. Refreshing never grants authorization or clears the extension reload
|
|
39
|
+
reminder. Keep Chrome and the paired extension running for this action.
|
|
40
|
+
|
|
41
|
+
Status also shows the **last observed** helper connection state. It never opens
|
|
42
|
+
a socket: use **Test** for a live health probe. Authorization and helper
|
|
43
|
+
reachability are reported separately; a connected helper does not by itself
|
|
44
|
+
mean the extension is ready or a conversation is authorized.
|
|
45
|
+
|
|
46
|
+
| Helper state | Meaning and next action |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `unknown` | No connection observation since this start; run Test. |
|
|
49
|
+
| `invalid_installation` | Pairing record validation failed; use **Chrome connection** in Settings to repair it, then run Test. Record contents are never shown. This state does not delay list attempts; the next successful connection restores `connected`. |
|
|
50
|
+
| `not_installed` | No pairing record; use **Chrome connection** in Settings, then run Test. |
|
|
51
|
+
| `unreachable` | Socket missing or connection refused; check Chrome and the extension, then run Test to retry now. Status includes the outage start, failure count and earliest next list attempt. |
|
|
52
|
+
| `connected` | A socket connection was accepted; status includes the last contact time. Run Test to check current helper/extension health. |
|
|
53
|
+
|
|
54
|
+
When the helper is unavailable, list returns `{ returns: [] }` and retries only
|
|
55
|
+
on a later Host poll after 2, 4, 8, 16, 32, then at most every 60 seconds. There
|
|
56
|
+
is no background retry timer. Append, acknowledge, Test and Refresh titles bypass this delay;
|
|
57
|
+
any accepted connection resets it, even when the helper returns a business
|
|
58
|
+
failure. Errors after a write are never treated as pre-send unavailability.
|
|
59
|
+
Only connection-state changes produce logs. Stop clears these observations and
|
|
60
|
+
cancels pending requests; it does not delete authorizations or pending replies.
|
|
61
|
+
|
|
62
|
+
## Set up Chrome from Settings
|
|
63
|
+
|
|
64
|
+
Open **Settings → Plugins → Personal Chrome → Chrome connection**. Choose the
|
|
65
|
+
setup action and confirm registration of the local helper. The Host verifies
|
|
66
|
+
the exact trusted release before invoking its fixed setup runner. Installation
|
|
67
|
+
creates the pairing identity privately; you do not copy an extension ID, secret,
|
|
68
|
+
or terminal command. Enabling a plugin alone never grants browser registration.
|
|
69
|
+
|
|
70
|
+
The card gives you the stable extension folder. For the first installation,
|
|
71
|
+
open Chrome's Extensions menu → Manage Extensions, enable Developer mode,
|
|
72
|
+
and choose **Load unpacked**, selecting that folder. Chrome requires this
|
|
73
|
+
browser-side action for an unpacked extension; an ordinary webpage cannot link
|
|
74
|
+
to `chrome://extensions`. This release does not claim Chrome Web Store delivery
|
|
75
|
+
or unattended extension installation.
|
|
76
|
+
|
|
77
|
+
Return to the card and run **Test connection**. “Installed” only confirms the
|
|
78
|
+
local registration and files. It does not mean Chrome is connected or that a
|
|
79
|
+
conversation is authorized. Keep Chrome running; the extension reconnects to
|
|
80
|
+
the helper. Test checks the real helper/extension revisions. Then open the exact
|
|
81
|
+
ChatGPT conversation you want to authorize and click the extension's toolbar
|
|
82
|
+
action. Its authorization will appear in Settings; use the row's revoke action
|
|
83
|
+
to remove it, or Refresh titles to retrieve its readable title.
|
|
84
|
+
|
|
85
|
+
The stable folder is `<projectRoot>/.cat-cafe/plugin-host/personal-chrome-host/extension/`.
|
|
86
|
+
On updates or rollbacks the package refreshes these bytes; **Reload** the already
|
|
87
|
+
loaded extension in Chrome, then run Test. If the helper has not been installed,
|
|
88
|
+
use setup first: reloading Chrome cannot create its registration. The reload
|
|
89
|
+
reminder clears only after a correlated reply proves the current revisions.
|
|
90
|
+
|
|
91
|
+
Setup can be retried without rotating a valid pairing identity. A registration
|
|
92
|
+
owned by another installation is a conflict, never silently overwritten. A broken
|
|
93
|
+
pairing record is reported honestly; removing and setting up again creates a new
|
|
94
|
+
identity. Setup removal refuses while the helper is active: close Chrome before
|
|
95
|
+
retrying. It removes only this installation's registration, launcher and pairing,
|
|
96
|
+
retaining authorizations, titles, extension files and delivery history. Revoke
|
|
97
|
+
individual authorizations explicitly when desired, and remove the extension in
|
|
98
|
+
Chrome if you no longer want it loaded.
|
|
99
|
+
|
|
100
|
+
The fixed protocol and authority boundary for Host integrators are documented in
|
|
101
|
+
[native-host/setup-protocol.md](native-host/setup-protocol.md). macOS and Linux
|
|
102
|
+
helper installation are supported; Windows setup is explicitly unsupported.
|
|
103
|
+
The initial Host product integration targets macOS.
|
|
104
|
+
|
|
105
|
+
## Authority boundary
|
|
106
|
+
|
|
107
|
+
Cat Café remains responsible for catalog/SRI/admission, pairing-secret
|
|
108
|
+
issuance and rotation, lifecycle supervision, Settings/onboarding, and
|
|
109
|
+
user-visible status. The installed helper receives a complete Host-supplied
|
|
110
|
+
pairing record or environment configuration; it never generates a secret or
|
|
111
|
+
chooses an install path.
|
|
112
|
+
|
|
113
|
+
The extension may bind only an explicitly clicked exact
|
|
114
|
+
`https://chatgpt.com/c/<id>` conversation. Normal dispatch finds that exact
|
|
115
|
+
background tab and sends the append request without focusing, navigating,
|
|
116
|
+
reloading, activating, selecting, moving, highlighting, reading cookies, or
|
|
117
|
+
calling a private ChatGPT API.
|
|
118
|
+
|
|
119
|
+
## Release status
|
|
120
|
+
|
|
121
|
+
This package is a review candidate only. It does not publish to npm or the
|
|
122
|
+
Chrome Web Store. Signed Chrome Web Store identity/admission and Windows support remain open; Windows is explicitly
|
|
123
|
+
unsupported.
|
|
124
|
+
|
|
125
|
+
Reading or revoking existing conversation authorizations is independent of runtime
|
|
126
|
+
enablement and uses the same data directory. These settings operations do not
|
|
127
|
+
install a helper, copy extension files or register a conversation runtime.
|
|
128
|
+
|
|
129
|
+
The Host owns installation, grants, credentials and lifecycle; saving configuration preserves enable intent. The language link opens a separate document and does not imply automatic language selection.
|
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 个人 Chrome 伴侣
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
简体中文 · [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
将你明确授权的 ChatGPT 网页对话接入 Clowder AI。插件提供 Chrome 扩展和本机 Native Messaging 辅助程序;授权精确到单个对话。
|
|
6
|
+
|
|
7
|
+
## 开始使用
|
|
8
|
+
|
|
9
|
+
1. 打开“设置 → 插件 → Personal Chrome → Chrome connection”,选择安装操作并确认注册本机辅助程序。启用插件本身不会授予浏览器注册权限。
|
|
10
|
+
2. 首次安装时,在 Chrome 扩展管理页开启开发者模式,选择“加载已解压的扩展”,使用设置卡片给出的固定扩展目录。无需复制扩展 ID、密钥或终端命令。
|
|
11
|
+
3. 返回设置运行连接测试,保持 Chrome 和扩展运行。显示已安装只证明本地文件与注册存在,不代表连接正常或对话已授权。
|
|
12
|
+
4. 打开你想授权的准确 ChatGPT 对话,点击扩展工具栏按钮。设置中会列出这条授权,可逐条撤销。
|
|
13
|
+
|
|
14
|
+
此发布不提供 Chrome 应用商店交付或无人值守安装。辅助程序支持 macOS、Linux,不支持 Windows;初始宿主集成以 macOS 为目标。
|
|
15
|
+
|
|
16
|
+
## 授权与状态
|
|
17
|
+
|
|
18
|
+
最多保存 32 条授权。“刷新标题”从已连接扩展读取已授权对话的标题,不新增授权。授权列表、撤销和状态读取独立于运行启用状态,使用同一份私有数据。
|
|
19
|
+
|
|
20
|
+
状态显示最近观察到的辅助程序连接,不会自动探测网络:用连接测试检查当前健康。`unknown` 表示尚未观察;`not_installed` 表示缺少配对;`invalid_installation` 表示配对记录无效;`unreachable` 表示连接不可达;`connected` 只表示曾成功接受连接。辅助程序可达不等于扩展或对话已就绪。
|
|
21
|
+
|
|
22
|
+
辅助程序不可达时,后续宿主轮询采用 2、4、8、16、32、最多 60 秒的退避,不建立后台重试定时器。发送、确认、测试和刷新标题可立即尝试。回复列出后仍须确认才移除;停止不会删除授权或待确认回复。
|
|
23
|
+
|
|
24
|
+
## 更新与卸除
|
|
25
|
+
|
|
26
|
+
升级或回退会刷新固定扩展目录的文件。请在 Chrome 中重新加载已加载的扩展,再测试;只有对应版本的关联回复确认后才清除重新加载提示。未安装辅助程序时,单纯重新加载扩展不能完成注册。
|
|
27
|
+
|
|
28
|
+
重试安装不轮换有效配对身份;属于其他安装的注册不会被静默覆盖。损坏配对需移除后重建。移除辅助程序前先关闭 Chrome;它只移除此安装的注册、启动器和配对,保留授权、标题、扩展文件及投递历史。需要时另行撤销授权并在 Chrome 移除扩展。
|
|
29
|
+
|
|
30
|
+
## 隐私边界
|
|
31
|
+
|
|
32
|
+
扩展只绑定明确点击的 `https://chatgpt.com/c/<id>` 对话。发送在准确的后台标签中完成,不主动切换、导航、刷新或读取 Cookie,也不调用私人 ChatGPT API。宿主发放配对身份和受限私有目录,包不会自行生成密钥或猜测安装位置。完整宿主安装和账号旅程需分别验收。
|
|
33
|
+
|
|
34
|
+
宿主管理安装、授权、凭据和启停;配置保存不自动改变启用意图。英文文档是独立文件,互链不代表页面已支持自动语言切换。
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Personal Chrome companion">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#1b72e8"/><circle cx="32" cy="32" r="18" fill="#fff"/><circle cx="32" cy="32" r="10" fill="#1b72e8"/><circle cx="32" cy="32" r="4" fill="#fff"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface PersonalChromeAssistantReturnCursor {
|
|
2
|
+
readonly conversationId: string;
|
|
3
|
+
readonly sourceMessageId: string;
|
|
4
|
+
readonly assistantMessageId: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PersonalChromeAssistantReturnCursorFields {
|
|
7
|
+
readonly afterConversationId?: string;
|
|
8
|
+
readonly afterSourceMessageId?: string;
|
|
9
|
+
readonly afterAssistantMessageId?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function assistantReturnCursorFields(cursor?: PersonalChromeAssistantReturnCursor): PersonalChromeAssistantReturnCursorFields;
|
|
12
|
+
export declare function parseAssistantReturnCursorFields(record: Readonly<Record<string, unknown>>): PersonalChromeAssistantReturnCursorFields;
|
|
13
|
+
//# sourceMappingURL=assistant-return-cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-return-cursor.d.ts","sourceRoot":"","sources":["../src/assistant-return-cursor.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,yCAAyC;IACxD,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAC3C;AAED,wBAAgB,2BAA2B,CACzC,MAAM,CAAC,EAAE,mCAAmC,GAC3C,yCAAyC,CAQ3C;AAED,wBAAgB,gCAAgC,CAC9C,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACxC,yCAAyC,CA0B3C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SAFE_TOKEN = /^[A-Za-z0-9._:-]+$/;
|
|
2
|
+
export function assistantReturnCursorFields(cursor) {
|
|
3
|
+
return cursor
|
|
4
|
+
? {
|
|
5
|
+
afterConversationId: cursor.conversationId,
|
|
6
|
+
afterSourceMessageId: cursor.sourceMessageId,
|
|
7
|
+
afterAssistantMessageId: cursor.assistantMessageId,
|
|
8
|
+
}
|
|
9
|
+
: {};
|
|
10
|
+
}
|
|
11
|
+
export function parseAssistantReturnCursorFields(record) {
|
|
12
|
+
const hasConversation = Object.hasOwn(record, 'afterConversationId');
|
|
13
|
+
const hasSource = Object.hasOwn(record, 'afterSourceMessageId');
|
|
14
|
+
const hasAssistant = Object.hasOwn(record, 'afterAssistantMessageId');
|
|
15
|
+
if (hasConversation !== hasSource || hasSource !== hasAssistant) {
|
|
16
|
+
throw new Error('assistant return list cursor must contain conversation, source, and assistant message IDs');
|
|
17
|
+
}
|
|
18
|
+
if (!hasConversation)
|
|
19
|
+
return {};
|
|
20
|
+
if (typeof record.afterConversationId !== 'string' ||
|
|
21
|
+
record.afterConversationId.length > 200 ||
|
|
22
|
+
!SAFE_TOKEN.test(record.afterConversationId) ||
|
|
23
|
+
typeof record.afterSourceMessageId !== 'string' ||
|
|
24
|
+
record.afterSourceMessageId.length > 512 ||
|
|
25
|
+
!SAFE_TOKEN.test(record.afterSourceMessageId) ||
|
|
26
|
+
typeof record.afterAssistantMessageId !== 'string' ||
|
|
27
|
+
record.afterAssistantMessageId.length > 512 ||
|
|
28
|
+
!SAFE_TOKEN.test(record.afterAssistantMessageId)) {
|
|
29
|
+
throw new Error('assistant return list cursor contains an invalid identity token');
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
afterConversationId: record.afterConversationId,
|
|
33
|
+
afterSourceMessageId: record.afterSourceMessageId,
|
|
34
|
+
afterAssistantMessageId: record.afterAssistantMessageId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=assistant-return-cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-return-cursor.js","sourceRoot":"","sources":["../src/assistant-return-cursor.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,oBAAoB,CAAC;AAcxC,MAAM,UAAU,2BAA2B,CACzC,MAA4C;IAE5C,OAAO,MAAM;QACX,CAAC,CAAC;YACE,mBAAmB,EAAE,MAAM,CAAC,cAAc;YAC1C,oBAAoB,EAAE,MAAM,CAAC,eAAe;YAC5C,uBAAuB,EAAE,MAAM,CAAC,kBAAkB;SACnD;QACH,CAAC,CAAC,EAAE,CAAC;AACT,CAAC;AAED,MAAM,UAAU,gCAAgC,CAC9C,MAAyC;IAEzC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IACrE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAChE,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;IACtE,IAAI,eAAe,KAAK,SAAS,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CAAC,2FAA2F,CAAC,CAAC;IAC/G,CAAC;IACD,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,CAAC;IAChC,IACE,OAAO,MAAM,CAAC,mBAAmB,KAAK,QAAQ;QAC9C,MAAM,CAAC,mBAAmB,CAAC,MAAM,GAAG,GAAG;QACvC,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC;QAC5C,OAAO,MAAM,CAAC,oBAAoB,KAAK,QAAQ;QAC/C,MAAM,CAAC,oBAAoB,CAAC,MAAM,GAAG,GAAG;QACxC,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC;QAC7C,OAAO,MAAM,CAAC,uBAAuB,KAAK,QAAQ;QAClD,MAAM,CAAC,uBAAuB,CAAC,MAAM,GAAG,GAAG;QAC3C,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAChD,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACrF,CAAC;IACD,OAAO;QACL,mBAAmB,EAAE,MAAM,CAAC,mBAAmB;QAC/C,oBAAoB,EAAE,MAAM,CAAC,oBAAoB;QACjD,uBAAuB,EAAE,MAAM,CAAC,uBAAuB;KACxD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CloudConversationAckResult, type CloudConversationAppendMessageResult, type CloudConversationListResult } from '@clowder-ai/plugin-contract';
|
|
2
|
+
import { type PersonalChromeHostAdapterOptions } from './personal-chrome-host-transport.js';
|
|
3
|
+
import { type HelperConnectionLog, type HelperConnectionStatus } from './helper-reachability.js';
|
|
4
|
+
import type { PersonalChromeTitleSync } from './title-refresh-protocol.js';
|
|
5
|
+
export interface PersonalChromeConversationHostOperations {
|
|
6
|
+
readonly refreshTitles: () => Promise<PersonalChromeTitleSync>;
|
|
7
|
+
readonly appendMessage: (input: unknown) => Promise<CloudConversationAppendMessageResult>;
|
|
8
|
+
readonly list: (input: unknown) => Promise<CloudConversationListResult>;
|
|
9
|
+
readonly ack: (input: unknown) => Promise<CloudConversationAckResult>;
|
|
10
|
+
/** Honest reachability probe for the `test` action: never claims ok without a real probe. */
|
|
11
|
+
readonly probe: () => Promise<{
|
|
12
|
+
readonly ok: boolean;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** Last-known connection state; never opens a socket or reads the pairing record. */
|
|
16
|
+
readonly status: () => HelperConnectionStatus;
|
|
17
|
+
readonly dispose: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The three cloud-conversation-host methods (contract beta.24 h3) backed by the
|
|
21
|
+
* package's own native-host socket client. None of these throw: the Host treats
|
|
22
|
+
* any throw after the call as AMBIGUOUS_EFFECT, so every failure is returned as
|
|
23
|
+
* data per the (d) error-code table and every result passes the contract
|
|
24
|
+
* validators with no extra fields.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createConversationHostOperations(options: {
|
|
27
|
+
readonly dataDirectory: string;
|
|
28
|
+
readonly timeoutMs?: number;
|
|
29
|
+
readonly now?: () => number;
|
|
30
|
+
readonly log?: HelperConnectionLog;
|
|
31
|
+
readonly onRevisionContact?: PersonalChromeHostAdapterOptions['onRevisionContact'];
|
|
32
|
+
}): PersonalChromeConversationHostOperations;
|
|
33
|
+
//# sourceMappingURL=conversation-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-host.d.ts","sourceRoot":"","sources":["../src/conversation-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,0BAA0B,EAC/B,KAAK,oCAAoC,EACzC,KAAK,2BAA2B,EAIjC,MAAM,6BAA6B,CAAC;AAOrC,OAAO,EAA6D,KAAK,gCAAgC,EAAE,MAAM,qCAAqC,CAAC;AAEvJ,OAAO,EAAsB,KAAK,mBAAmB,EAAE,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACrH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAE3E,MAAM,WAAW,wCAAwC;IACvD,QAAQ,CAAC,aAAa,EAAE,MAAM,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC/D,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,oCAAoC,CAAC,CAAC;IAC1F,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACxE,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACtE,6FAA6F;IAC7F,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClF,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,sBAAsB,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AA0DD;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAAC,OAAO,EAAE;IACxD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,gCAAgC,CAAC,mBAAmB,CAAC,CAAC;CACpF,GAAG,wCAAwC,CA8K3C"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { isCloudBridgeFailureDiagnosticV1, isCloudConversationAckInput, isCloudConversationAppendMessageInput, } from '@clowder-ai/plugin-contract';
|
|
2
|
+
import { createPersonalChromeHostAdapter, isPersonalChromeNotInstalled, readPersonalChromeAdapterOptions, } from './pairing-record.js';
|
|
3
|
+
import { PersonalChromeHostError, PersonalChromeHostRequestTracker } from './personal-chrome-host-transport.js';
|
|
4
|
+
import { HelperReachability } from './helper-reachability.js';
|
|
5
|
+
function isObjectRecord(value) {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function asContractDiagnostic(diagnostic) {
|
|
9
|
+
return isCloudBridgeFailureDiagnosticV1(diagnostic) ? { diagnostic } : {};
|
|
10
|
+
}
|
|
11
|
+
function appendFailure(errorCode, extras = {}) {
|
|
12
|
+
return { status: 'failed', errorCode, ...extras };
|
|
13
|
+
}
|
|
14
|
+
function ackFailure(errorCode, extras = {}) {
|
|
15
|
+
return { status: 'failed', errorCode, ...extras };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the pairing record from the granted data directory. A missing record
|
|
19
|
+
* is one of the three (e) cases and maps to HOST_UNAVAILABLE; a corrupt record is
|
|
20
|
+
* INVALID_CONFIGURATION. Both are PersonalChromeHostError codes passed through.
|
|
21
|
+
*/
|
|
22
|
+
async function resolveAdapterOptions(dataDirectory, onNotInstalled, onInvalidConfiguration) {
|
|
23
|
+
try {
|
|
24
|
+
return await readPersonalChromeAdapterOptions(dataDirectory);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (isPersonalChromeNotInstalled(error)) {
|
|
28
|
+
onNotInstalled();
|
|
29
|
+
throw new PersonalChromeHostError('HOST_UNAVAILABLE', 'personal Chrome host is not installed');
|
|
30
|
+
}
|
|
31
|
+
if (error instanceof PersonalChromeHostError && error.code === 'INVALID_CONFIGURATION') {
|
|
32
|
+
onInvalidConfiguration();
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function toCursor(value) {
|
|
38
|
+
if (!isObjectRecord(value))
|
|
39
|
+
return undefined;
|
|
40
|
+
const { conversationId, sourceMessageId, assistantMessageId } = value;
|
|
41
|
+
if (typeof conversationId !== 'string' ||
|
|
42
|
+
typeof sourceMessageId !== 'string' ||
|
|
43
|
+
typeof assistantMessageId !== 'string') {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return { conversationId, sourceMessageId, assistantMessageId };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The three cloud-conversation-host methods (contract beta.24 h3) backed by the
|
|
50
|
+
* package's own native-host socket client. None of these throw: the Host treats
|
|
51
|
+
* any throw after the call as AMBIGUOUS_EFFECT, so every failure is returned as
|
|
52
|
+
* data per the (d) error-code table and every result passes the contract
|
|
53
|
+
* validators with no extra fields.
|
|
54
|
+
*/
|
|
55
|
+
export function createConversationHostOperations(options) {
|
|
56
|
+
const { dataDirectory } = options;
|
|
57
|
+
let disposed = false;
|
|
58
|
+
// Cancels every in-flight helper request on stop: dispose destroys their
|
|
59
|
+
// sockets and settles the pending promises (written ones as AMBIGUOUS_EFFECT)
|
|
60
|
+
// instead of leaking them until their timeouts (ledger h3 (e)).
|
|
61
|
+
const requestTracker = new PersonalChromeHostRequestTracker();
|
|
62
|
+
const reachability = new HelperReachability(options.now ?? Date.now, options.log);
|
|
63
|
+
const unavailable = (state) => {
|
|
64
|
+
if (!disposed)
|
|
65
|
+
reachability.unavailable(state);
|
|
66
|
+
};
|
|
67
|
+
const adapterOptionsForRequest = async () => ({
|
|
68
|
+
...(await resolveAdapterOptions(dataDirectory, () => unavailable('not_installed'), () => { if (!disposed)
|
|
69
|
+
reachability.invalidInstallation(); })),
|
|
70
|
+
timeoutMs: options.timeoutMs,
|
|
71
|
+
requestTracker,
|
|
72
|
+
onConnected: () => { if (!disposed)
|
|
73
|
+
reachability.connected(); },
|
|
74
|
+
onUnavailable: () => unavailable('unreachable'),
|
|
75
|
+
onRevisionContact: async (revisions, errorCode) => {
|
|
76
|
+
if (!disposed)
|
|
77
|
+
await options.onRevisionContact?.(revisions, errorCode);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const appendMessage = async (input) => {
|
|
81
|
+
if (disposed)
|
|
82
|
+
return appendFailure('HOST_UNAVAILABLE');
|
|
83
|
+
if (!isCloudConversationAppendMessageInput(input)) {
|
|
84
|
+
return appendFailure('INVALID_REQUEST');
|
|
85
|
+
}
|
|
86
|
+
let adapterOptions;
|
|
87
|
+
try {
|
|
88
|
+
adapterOptions = await adapterOptionsForRequest();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof PersonalChromeHostError)
|
|
92
|
+
return appendFailure(error.code);
|
|
93
|
+
return appendFailure('AMBIGUOUS_EFFECT');
|
|
94
|
+
}
|
|
95
|
+
// Stop may have run while the pairing record was being read: no socket has
|
|
96
|
+
// been created yet, so nothing was sent and HOST_UNAVAILABLE stays truthful
|
|
97
|
+
// (cancelAll alone cannot cover requests that register after it returns).
|
|
98
|
+
if (disposed)
|
|
99
|
+
return appendFailure('HOST_UNAVAILABLE');
|
|
100
|
+
try {
|
|
101
|
+
const receipt = await createPersonalChromeHostAdapter(adapterOptions).append_message(input.conversationId, input.text, input.idempotencyKey);
|
|
102
|
+
return {
|
|
103
|
+
status: 'appended',
|
|
104
|
+
providerMessageId: receipt.hostMessageId,
|
|
105
|
+
...(receipt.idempotentReplay === undefined ? {} : { idempotentReplay: receipt.idempotentReplay }),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof PersonalChromeHostError) {
|
|
110
|
+
return appendFailure(error.code, {
|
|
111
|
+
...asContractDiagnostic(error.diagnostic),
|
|
112
|
+
...(error.idempotentReplay === undefined ? {} : { idempotentReplay: error.idempotentReplay }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// The request may have reached the helper; without a typed code the only
|
|
116
|
+
// truthful answer is "effect unknown" (ledger h3 (e)).
|
|
117
|
+
return appendFailure('AMBIGUOUS_EFFECT');
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const list = async (input) => {
|
|
121
|
+
if (disposed || !reachability.canList())
|
|
122
|
+
return { returns: [] };
|
|
123
|
+
let adapterOptions;
|
|
124
|
+
try {
|
|
125
|
+
adapterOptions = await adapterOptionsForRequest();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Invalid installation is visible in status but does not start backoff.
|
|
129
|
+
// Only missing installation or a pre-send socket failure gates list.
|
|
130
|
+
return { returns: [] };
|
|
131
|
+
}
|
|
132
|
+
// Stop during pairing resolution: nothing sent yet, report an empty round.
|
|
133
|
+
if (disposed)
|
|
134
|
+
return { returns: [] };
|
|
135
|
+
try {
|
|
136
|
+
const after = toCursor(isObjectRecord(input) ? input.after : undefined);
|
|
137
|
+
const returns = await createPersonalChromeHostAdapter(adapterOptions).list_assistant_returns(after);
|
|
138
|
+
return { returns: [...returns] };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { returns: [] };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const ack = async (input) => {
|
|
145
|
+
if (disposed)
|
|
146
|
+
return ackFailure('HOST_UNAVAILABLE');
|
|
147
|
+
if (!isCloudConversationAckInput(input)) {
|
|
148
|
+
return ackFailure('INVALID_REQUEST');
|
|
149
|
+
}
|
|
150
|
+
let adapterOptions;
|
|
151
|
+
try {
|
|
152
|
+
adapterOptions = await adapterOptionsForRequest();
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof PersonalChromeHostError)
|
|
156
|
+
return ackFailure(error.code);
|
|
157
|
+
return ackFailure('AMBIGUOUS_EFFECT');
|
|
158
|
+
}
|
|
159
|
+
// Stop during pairing resolution: no socket created, nothing was sent.
|
|
160
|
+
if (disposed)
|
|
161
|
+
return ackFailure('HOST_UNAVAILABLE');
|
|
162
|
+
try {
|
|
163
|
+
await createPersonalChromeHostAdapter(adapterOptions).ack_assistant_return(input.conversationId, input.sourceMessageId, input.assistantMessageId);
|
|
164
|
+
return { status: 'acknowledged' };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof PersonalChromeHostError) {
|
|
168
|
+
// ASSISTANT_RETURN_NOT_FOUND means the return is already settled; pass it
|
|
169
|
+
// through so the Host stops retrying that entry.
|
|
170
|
+
return ackFailure(error.code, asContractDiagnostic(error.diagnostic));
|
|
171
|
+
}
|
|
172
|
+
return ackFailure('AMBIGUOUS_EFFECT');
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const probe = async () => {
|
|
176
|
+
if (disposed)
|
|
177
|
+
return { ok: false, message: 'personal Chrome host module is stopped' };
|
|
178
|
+
let adapterOptions;
|
|
179
|
+
try {
|
|
180
|
+
adapterOptions = await adapterOptionsForRequest();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof PersonalChromeHostError && error.code === 'HOST_UNAVAILABLE') {
|
|
184
|
+
return { ok: false, message: 'personal Chrome host is not installed (no pairing record)' };
|
|
185
|
+
}
|
|
186
|
+
if (error instanceof PersonalChromeHostError) {
|
|
187
|
+
return { ok: false, message: `personal Chrome pairing record rejected: ${error.code}` };
|
|
188
|
+
}
|
|
189
|
+
return { ok: false, message: 'personal Chrome pairing record is unreadable' };
|
|
190
|
+
}
|
|
191
|
+
// Stop during pairing resolution: never probe after stop returned.
|
|
192
|
+
if (disposed)
|
|
193
|
+
return { ok: false, message: 'personal Chrome host module is stopped' };
|
|
194
|
+
try {
|
|
195
|
+
const health = await createPersonalChromeHostAdapter(adapterOptions).check_health();
|
|
196
|
+
if (health.status === 'ready') {
|
|
197
|
+
return { ok: true, message: 'personal Chrome host helper answered the health probe' };
|
|
198
|
+
}
|
|
199
|
+
const reason = health.errorCode ?? health.status;
|
|
200
|
+
return { ok: false, message: `personal Chrome host health probe reported ${reason}` };
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error instanceof PersonalChromeHostError) {
|
|
204
|
+
return { ok: false, message: `personal Chrome host is not reachable: ${error.code}` };
|
|
205
|
+
}
|
|
206
|
+
return { ok: false, message: 'personal Chrome host health probe failed unexpectedly' };
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
// Owner-triggered, like probe: deliberately bypasses the background list gate.
|
|
210
|
+
const refreshTitles = async () => {
|
|
211
|
+
if (disposed)
|
|
212
|
+
return { status: 'unavailable', errorCode: 'HOST_UNAVAILABLE' };
|
|
213
|
+
try {
|
|
214
|
+
const adapterOptions = await adapterOptionsForRequest();
|
|
215
|
+
if (disposed)
|
|
216
|
+
return { status: 'unavailable', errorCode: 'HOST_UNAVAILABLE' };
|
|
217
|
+
return await createPersonalChromeHostAdapter(adapterOptions).refresh_conversation_titles();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return { status: 'unavailable', errorCode: error instanceof PersonalChromeHostError ? error.code : 'AMBIGUOUS_EFFECT' };
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
refreshTitles,
|
|
225
|
+
appendMessage,
|
|
226
|
+
list,
|
|
227
|
+
ack,
|
|
228
|
+
probe,
|
|
229
|
+
status: () => reachability.snapshot(),
|
|
230
|
+
dispose: async () => {
|
|
231
|
+
disposed = true;
|
|
232
|
+
reachability.reset();
|
|
233
|
+
requestTracker.cancelAll();
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=conversation-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-host.js","sourceRoot":"","sources":["../src/conversation-host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** No symlinks: copying a package must not expand its read/write authority. */
|
|
2
|
+
export declare function extensionFiles(directory: string): Promise<Map<string, Buffer>>;
|
|
3
|
+
export declare function extensionDigest(files: ReadonlyMap<string, Buffer>): string;
|
|
4
|
+
/** Caller holds the install lease. The previous complete directory is recoverable until activation. */
|
|
5
|
+
export declare function replaceExtension<T>(dataDirectory: string, files: ReadonlyMap<string, Buffer>, activate: () => Promise<T>): Promise<T>;
|
|
6
|
+
//# sourceMappingURL=extension-delivery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-delivery.d.ts","sourceRoot":"","sources":["../src/extension-delivery.ts"],"names":[],"mappings":"AAIA,+EAA+E;AAC/E,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcpF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAI1E;AAED,uGAAuG;AACvG,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACjG,QAAQ,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgCxC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
/** No symlinks: copying a package must not expand its read/write authority. */
|
|
5
|
+
export async function extensionFiles(directory) {
|
|
6
|
+
const files = new Map();
|
|
7
|
+
const visit = async (relative) => {
|
|
8
|
+
const path = join(directory, relative);
|
|
9
|
+
const metadata = await lstat(path);
|
|
10
|
+
if (metadata.isSymbolicLink())
|
|
11
|
+
throw new Error('extension symlinks are not supported');
|
|
12
|
+
if (metadata.isDirectory()) {
|
|
13
|
+
for (const child of (await readdir(path)).sort())
|
|
14
|
+
await visit(join(relative, child));
|
|
15
|
+
}
|
|
16
|
+
else if (metadata.isFile()) {
|
|
17
|
+
files.set(relative, await readFile(path));
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
throw new Error('extension entry must be a regular file or directory');
|
|
21
|
+
};
|
|
22
|
+
await visit('');
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
export function extensionDigest(files) {
|
|
26
|
+
const hash = createHash('sha256');
|
|
27
|
+
for (const [name, bytes] of files)
|
|
28
|
+
hash.update(name).update('\0').update(bytes).update('\0');
|
|
29
|
+
return hash.digest('hex');
|
|
30
|
+
}
|
|
31
|
+
/** Caller holds the install lease. The previous complete directory is recoverable until activation. */
|
|
32
|
+
export async function replaceExtension(dataDirectory, files, activate) {
|
|
33
|
+
const destination = join(dataDirectory, 'extension');
|
|
34
|
+
const staging = join(dataDirectory, `.extension-${randomUUID()}`);
|
|
35
|
+
const backup = join(dataDirectory, `.extension-previous-${randomUUID()}`);
|
|
36
|
+
let backedUp = false;
|
|
37
|
+
let activated = false;
|
|
38
|
+
await mkdir(staging, { mode: 0o700 });
|
|
39
|
+
try {
|
|
40
|
+
for (const [name, bytes] of files) {
|
|
41
|
+
await mkdir(dirname(join(staging, name)), { recursive: true, mode: 0o700 });
|
|
42
|
+
await writeFile(join(staging, name), bytes, { mode: 0o600, flag: 'wx' });
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await rename(destination, backup);
|
|
46
|
+
backedUp = true;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== 'ENOENT')
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await rename(staging, destination);
|
|
54
|
+
const result = await activate();
|
|
55
|
+
activated = true;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
await rm(destination, { recursive: true, force: true });
|
|
60
|
+
if (backedUp)
|
|
61
|
+
await rename(backup, destination);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await rm(staging, { recursive: true, force: true });
|
|
67
|
+
if (activated && backedUp)
|
|
68
|
+
await rm(backup, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=extension-delivery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-delivery.js","sourceRoot":"","sources":["../src/extension-delivery.ts"],"names":[],"mappings":"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"}
|