@greppleai/grip 0.0.1 → 0.1.3

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 (197) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +13 -7
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js +4 -2
  5. package/dist/bun/cli.js.map +1 -1
  6. package/dist/bun/embedded-assets.d.ts +3 -0
  7. package/dist/bun/embedded-assets.d.ts.map +1 -0
  8. package/dist/bun/embedded-assets.js +52 -0
  9. package/dist/bun/embedded-assets.js.map +1 -0
  10. package/dist/cli/args.d.ts.map +1 -1
  11. package/dist/cli/args.js +1 -0
  12. package/dist/cli/args.js.map +1 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +3 -3
  15. package/dist/config.js.map +1 -1
  16. package/dist/core/agent-session.d.ts.map +1 -1
  17. package/dist/core/agent-session.js +17 -2
  18. package/dist/core/agent-session.js.map +1 -1
  19. package/dist/core/remote-workspace-protocol/contract.d.ts +13 -4
  20. package/dist/core/remote-workspace-protocol/contract.d.ts.map +1 -1
  21. package/dist/core/remote-workspace-protocol/contract.js +17 -1
  22. package/dist/core/remote-workspace-protocol/contract.js.map +1 -1
  23. package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts +16 -0
  24. package/dist/core/remote-workspace-protocol/guest-shared-client.d.ts.map +1 -0
  25. package/dist/core/remote-workspace-protocol/guest-shared-client.js +54 -0
  26. package/dist/core/remote-workspace-protocol/guest-shared-client.js.map +1 -0
  27. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts +23 -0
  28. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.d.ts.map +1 -0
  29. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js +112 -0
  30. package/dist/core/remote-workspace-protocol/guest-shared-outbound-scheduler.js.map +1 -0
  31. package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts +30 -0
  32. package/dist/core/remote-workspace-protocol/guest-shared-uplink.d.ts.map +1 -0
  33. package/dist/core/remote-workspace-protocol/guest-shared-uplink.js +243 -0
  34. package/dist/core/remote-workspace-protocol/guest-shared-uplink.js.map +1 -0
  35. package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts +3 -0
  36. package/dist/core/remote-workspace-protocol/lima-owner-directory.d.ts.map +1 -0
  37. package/dist/core/remote-workspace-protocol/lima-owner-directory.js +31 -0
  38. package/dist/core/remote-workspace-protocol/lima-owner-directory.js.map +1 -0
  39. package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts +9 -0
  40. package/dist/core/remote-workspace-protocol/lima-owner-lock.d.ts.map +1 -0
  41. package/dist/core/remote-workspace-protocol/lima-owner-lock.js +89 -0
  42. package/dist/core/remote-workspace-protocol/lima-owner-lock.js.map +1 -0
  43. package/dist/core/remote-workspace-protocol/session.d.ts +2 -0
  44. package/dist/core/remote-workspace-protocol/session.d.ts.map +1 -1
  45. package/dist/core/remote-workspace-protocol/session.js +3 -0
  46. package/dist/core/remote-workspace-protocol/session.js.map +1 -1
  47. package/dist/core/remote-workspace-protocol/shared-transport.d.ts +71 -0
  48. package/dist/core/remote-workspace-protocol/shared-transport.d.ts.map +1 -0
  49. package/dist/core/remote-workspace-protocol/shared-transport.js +68 -0
  50. package/dist/core/remote-workspace-protocol/shared-transport.js.map +1 -0
  51. package/dist/core/sandbox/command.d.ts +2 -4
  52. package/dist/core/sandbox/command.d.ts.map +1 -1
  53. package/dist/core/sandbox/command.js +24 -46
  54. package/dist/core/sandbox/command.js.map +1 -1
  55. package/dist/core/sandbox/config.d.ts +91 -0
  56. package/dist/core/sandbox/config.d.ts.map +1 -0
  57. package/dist/core/sandbox/config.js +88 -0
  58. package/dist/core/sandbox/config.js.map +1 -0
  59. package/dist/core/sandbox/lima-guest-binary.d.ts +11 -0
  60. package/dist/core/sandbox/lima-guest-binary.d.ts.map +1 -0
  61. package/dist/core/sandbox/lima-guest-binary.js +140 -0
  62. package/dist/core/sandbox/lima-guest-binary.js.map +1 -0
  63. package/dist/core/sandbox/lima.d.ts +22 -3
  64. package/dist/core/sandbox/lima.d.ts.map +1 -1
  65. package/dist/core/sandbox/lima.js +434 -42
  66. package/dist/core/sandbox/lima.js.map +1 -1
  67. package/dist/core/sandbox/shared-lima-roots.d.ts +11 -0
  68. package/dist/core/sandbox/shared-lima-roots.d.ts.map +1 -0
  69. package/dist/core/sandbox/shared-lima-roots.js +25 -0
  70. package/dist/core/sandbox/shared-lima-roots.js.map +1 -0
  71. package/dist/core/sandbox/update-shared-guest-binary.d.ts +8 -0
  72. package/dist/core/sandbox/update-shared-guest-binary.d.ts.map +1 -0
  73. package/dist/core/sandbox/update-shared-guest-binary.js +100 -0
  74. package/dist/core/sandbox/update-shared-guest-binary.js.map +1 -0
  75. package/dist/core/settings-manager.d.ts +1 -5
  76. package/dist/core/settings-manager.d.ts.map +1 -1
  77. package/dist/core/settings-manager.js.map +1 -1
  78. package/dist/core/tools/bash.d.ts.map +1 -1
  79. package/dist/core/tools/bash.js +25 -14
  80. package/dist/core/tools/bash.js.map +1 -1
  81. package/dist/core/tools/file-mutation-queue.d.ts +2 -0
  82. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  83. package/dist/core/tools/file-mutation-queue.js +9 -0
  84. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  85. package/dist/core/tools/operations.d.ts +13 -2
  86. package/dist/core/tools/operations.d.ts.map +1 -1
  87. package/dist/core/tools/operations.js +132 -36
  88. package/dist/core/tools/operations.js.map +1 -1
  89. package/dist/daemon/activity-budget.d.ts +10 -0
  90. package/dist/daemon/activity-budget.d.ts.map +1 -0
  91. package/dist/daemon/activity-budget.js +34 -0
  92. package/dist/daemon/activity-budget.js.map +1 -0
  93. package/dist/daemon/config.d.ts +2 -0
  94. package/dist/daemon/config.d.ts.map +1 -1
  95. package/dist/daemon/config.js +15 -0
  96. package/dist/daemon/config.js.map +1 -1
  97. package/dist/daemon/outbound-budget.d.ts +10 -0
  98. package/dist/daemon/outbound-budget.d.ts.map +1 -0
  99. package/dist/daemon/outbound-budget.js +30 -0
  100. package/dist/daemon/outbound-budget.js.map +1 -0
  101. package/dist/daemon/security.d.ts.map +1 -1
  102. package/dist/daemon/security.js +3 -1
  103. package/dist/daemon/security.js.map +1 -1
  104. package/dist/daemon/server.d.ts.map +1 -1
  105. package/dist/daemon/server.js +104 -24
  106. package/dist/daemon/server.js.map +1 -1
  107. package/dist/daemon/shared-connection.d.ts +33 -0
  108. package/dist/daemon/shared-connection.d.ts.map +1 -0
  109. package/dist/daemon/shared-connection.js +271 -0
  110. package/dist/daemon/shared-connection.js.map +1 -0
  111. package/dist/daemon/shared-revision.d.ts +5 -0
  112. package/dist/daemon/shared-revision.d.ts.map +1 -0
  113. package/dist/daemon/shared-revision.js +61 -0
  114. package/dist/daemon/shared-revision.js.map +1 -0
  115. package/dist/daemon/shared-roots.d.ts +9 -0
  116. package/dist/daemon/shared-roots.d.ts.map +1 -0
  117. package/dist/daemon/shared-roots.js +33 -0
  118. package/dist/daemon/shared-roots.js.map +1 -0
  119. package/dist/daemon/workspace-runtime.d.ts +1 -1
  120. package/dist/daemon/workspace-runtime.d.ts.map +1 -1
  121. package/dist/daemon/workspace-runtime.js +42 -10
  122. package/dist/daemon/workspace-runtime.js.map +1 -1
  123. package/dist/main.d.ts.map +1 -1
  124. package/dist/main.js +5 -3
  125. package/dist/main.js.map +1 -1
  126. package/dist/modes/interactive/components/assistant-message.d.ts +1 -0
  127. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  128. package/dist/modes/interactive/components/assistant-message.js +13 -3
  129. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  130. package/dist/modes/interactive/components/branch-summary-message.d.ts +4 -3
  131. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/branch-summary-message.js +5 -4
  133. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  134. package/dist/modes/interactive/components/compaction-summary-message.d.ts +4 -3
  135. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  136. package/dist/modes/interactive/components/compaction-summary-message.js +5 -4
  137. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  138. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  139. package/dist/modes/interactive/components/custom-message.js +4 -3
  140. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  141. package/dist/modes/interactive/components/hook-execution.d.ts.map +1 -1
  142. package/dist/modes/interactive/components/hook-execution.js +3 -2
  143. package/dist/modes/interactive/components/hook-execution.js.map +1 -1
  144. package/dist/modes/interactive/components/message-card.d.ts +9 -0
  145. package/dist/modes/interactive/components/message-card.d.ts.map +1 -0
  146. package/dist/modes/interactive/components/message-card.js +24 -0
  147. package/dist/modes/interactive/components/message-card.js.map +1 -0
  148. package/dist/modes/interactive/components/skill-invocation-message.d.ts +4 -3
  149. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/skill-invocation-message.js +5 -4
  151. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  152. package/dist/modes/interactive/components/tool-execution.d.ts +1 -1
  153. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/tool-execution.js +27 -21
  155. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  156. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/user-message.js +3 -2
  158. package/dist/modes/interactive/components/user-message.js.map +1 -1
  159. package/dist/modes/interactive/interactive-mode.d.ts +0 -3
  160. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  161. package/dist/modes/interactive/interactive-mode.js +55 -44
  162. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  163. package/dist/modes/interactive/theme/theme.d.ts +3 -0
  164. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  165. package/dist/modes/interactive/theme/theme.js +10 -0
  166. package/dist/modes/interactive/theme/theme.js.map +1 -1
  167. package/dist/utils/clipboard-native.d.ts.map +1 -1
  168. package/dist/utils/clipboard-native.js +1 -1
  169. package/dist/utils/clipboard-native.js.map +1 -1
  170. package/dist/utils/image-resize.d.ts.map +1 -1
  171. package/dist/utils/image-resize.js +5 -0
  172. package/dist/utils/image-resize.js.map +1 -1
  173. package/dist/utils/photon.d.ts.map +1 -1
  174. package/dist/utils/photon.js +1 -0
  175. package/dist/utils/photon.js.map +1 -1
  176. package/docs/daemon.md +6 -4
  177. package/docs/docs.json +1 -1
  178. package/docs/index.md +8 -12
  179. package/docs/packages.md +3 -3
  180. package/docs/quickstart.md +11 -19
  181. package/docs/sandbox.md +40 -117
  182. package/docs/settings.md +12 -11
  183. package/docs/termux.md +1 -18
  184. package/docs/usage.md +2 -2
  185. package/examples/README.md +0 -2
  186. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  187. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  188. package/examples/extensions/sandbox/package.json +1 -1
  189. package/examples/extensions/with-deps/package.json +1 -1
  190. package/npm-shrinkwrap.json +12 -12
  191. package/package.json +5 -6
  192. package/dist/core/sandbox/docker.d.ts +0 -169
  193. package/dist/core/sandbox/docker.d.ts.map +0 -1
  194. package/dist/core/sandbox/docker.js +0 -633
  195. package/dist/core/sandbox/docker.js.map +0 -1
  196. package/examples/sandbox-docker/Dockerfile +0 -274
  197. package/examples/sandbox-docker/README.md +0 -88
@@ -0,0 +1,112 @@
1
+ import { DEFAULT_REMOTE_WORKSPACE_PROTOCOL_LIMITS as limits } from "./contract.js";
2
+ /** FIFO within each channel, round-robin between channels, including ordered detaches. */
3
+ export class GuestOutboundScheduler {
4
+ buckets = new Map();
5
+ ready = [];
6
+ pending = new Set();
7
+ pendingBytes = 0;
8
+ inFlight;
9
+ scheduled = false;
10
+ failure;
11
+ writer;
12
+ bufferedAmount;
13
+ onFailure;
14
+ constructor(writer, bufferedAmount, onFailure) {
15
+ this.writer = writer;
16
+ this.bufferedAmount = bufferedAmount;
17
+ this.onFailure = onFailure;
18
+ }
19
+ schedule() {
20
+ if (this.scheduled || this.failure)
21
+ return;
22
+ this.scheduled = true;
23
+ queueMicrotask(() => {
24
+ this.scheduled = false;
25
+ this.pump();
26
+ });
27
+ }
28
+ pump() {
29
+ if (this.failure || this.inFlight)
30
+ return;
31
+ const key = this.ready.shift();
32
+ if (!key)
33
+ return;
34
+ const bucket = this.buckets.get(key);
35
+ const job = bucket.shift();
36
+ if (bucket.length)
37
+ this.ready.push(key);
38
+ else
39
+ this.buckets.delete(key);
40
+ this.inFlight = job;
41
+ try {
42
+ this.writer(job.bytes, (error) => {
43
+ if (error)
44
+ this.onFailure(new Error("Shared workspace uplink send failed; operations may have committed"));
45
+ else
46
+ job.finish();
47
+ this.schedule();
48
+ });
49
+ }
50
+ catch {
51
+ this.onFailure(new Error("Shared workspace uplink send failed; operations may have committed"));
52
+ }
53
+ }
54
+ send(frame) {
55
+ if (this.failure)
56
+ return Promise.reject(this.failure);
57
+ const bytes = Buffer.from(JSON.stringify(frame));
58
+ const key = frame.type === "open" ? `open:${frame.id}` : frame.channelId;
59
+ const bucket = this.buckets.get(key);
60
+ const perChannel = (bucket?.length ?? 0) + (this.inFlight?.key === key ? 1 : 0);
61
+ if (bytes.length > limits.maxMessageBytes ||
62
+ this.pending.size >= limits.maxPendingOutboundMessages ||
63
+ perChannel >= 32 ||
64
+ this.pendingBytes + bytes.length + this.bufferedAmount() > limits.maxPendingOutboundBytes) {
65
+ this.onFailure(new Error("Shared workspace uplink output budget exceeded; operations may have committed"));
66
+ return Promise.reject(this.failure ?? new Error("Shared workspace uplink output budget exceeded"));
67
+ }
68
+ return new Promise((resolve, reject) => {
69
+ let finished = false;
70
+ const timer = setTimeout(() => {
71
+ this.onFailure(new Error("Shared workspace uplink send timed out; operations may have committed"));
72
+ }, limits.maxTransportSendMs);
73
+ const job = {
74
+ bytes,
75
+ key,
76
+ finish: (error) => {
77
+ if (finished)
78
+ return;
79
+ finished = true;
80
+ clearTimeout(timer);
81
+ this.pending.delete(job);
82
+ this.pendingBytes -= bytes.length;
83
+ if (this.inFlight === job)
84
+ this.inFlight = undefined;
85
+ if (error)
86
+ reject(error);
87
+ else
88
+ resolve();
89
+ },
90
+ };
91
+ this.pending.add(job);
92
+ this.pendingBytes += bytes.length;
93
+ if (bucket)
94
+ bucket.push(job);
95
+ else {
96
+ this.buckets.set(key, [job]);
97
+ this.ready.push(key);
98
+ }
99
+ this.schedule();
100
+ });
101
+ }
102
+ fail(error) {
103
+ if (this.failure)
104
+ return;
105
+ this.failure = error;
106
+ for (const job of this.pending)
107
+ job.finish(error);
108
+ this.buckets.clear();
109
+ this.ready.length = 0;
110
+ }
111
+ }
112
+ //# sourceMappingURL=guest-shared-outbound-scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guest-shared-outbound-scheduler.js","sourceRoot":"","sources":["../../../src/core/remote-workspace-protocol/guest-shared-outbound-scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_REMOTE_WORKSPACE_PROTOCOL_LIMITS as limits } from \"./contract.ts\";\nimport type { SharedWorkspaceClientFrame } from \"./shared-transport.ts\";\n\nexport type GuestOutboundFrame = Exclude<SharedWorkspaceClientFrame, { type: \"hello\" }>;\n\ntype Job = { bytes: Buffer; key: string; finish: (error?: Error) => void };\n\n/** FIFO within each channel, round-robin between channels, including ordered detaches. */\nexport class GuestOutboundScheduler {\n\tprivate readonly buckets = new Map<string, Job[]>();\n\tprivate readonly ready: string[] = [];\n\tprivate readonly pending = new Set<Job>();\n\tprivate pendingBytes = 0;\n\tprivate inFlight?: Job;\n\tprivate scheduled = false;\n\tprivate failure?: Error;\n\tprivate readonly writer: (bytes: Buffer, callback: (error?: Error) => void) => void;\n\tprivate readonly bufferedAmount: () => number;\n\tprivate readonly onFailure: (error: Error) => void;\n\n\tconstructor(\n\t\twriter: (bytes: Buffer, callback: (error?: Error) => void) => void,\n\t\tbufferedAmount: () => number,\n\t\tonFailure: (error: Error) => void,\n\t) {\n\t\tthis.writer = writer;\n\t\tthis.bufferedAmount = bufferedAmount;\n\t\tthis.onFailure = onFailure;\n\t}\n\n\tprivate schedule(): void {\n\t\tif (this.scheduled || this.failure) return;\n\t\tthis.scheduled = true;\n\t\tqueueMicrotask(() => {\n\t\t\tthis.scheduled = false;\n\t\t\tthis.pump();\n\t\t});\n\t}\n\n\tprivate pump(): void {\n\t\tif (this.failure || this.inFlight) return;\n\t\tconst key = this.ready.shift();\n\t\tif (!key) return;\n\t\tconst bucket = this.buckets.get(key)!;\n\t\tconst job = bucket.shift()!;\n\t\tif (bucket.length) this.ready.push(key);\n\t\telse this.buckets.delete(key);\n\t\tthis.inFlight = job;\n\t\ttry {\n\t\t\tthis.writer(job.bytes, (error) => {\n\t\t\t\tif (error) this.onFailure(new Error(\"Shared workspace uplink send failed; operations may have committed\"));\n\t\t\t\telse job.finish();\n\t\t\t\tthis.schedule();\n\t\t\t});\n\t\t} catch {\n\t\t\tthis.onFailure(new Error(\"Shared workspace uplink send failed; operations may have committed\"));\n\t\t}\n\t}\n\n\tsend(frame: GuestOutboundFrame): Promise<void> {\n\t\tif (this.failure) return Promise.reject(this.failure);\n\t\tconst bytes = Buffer.from(JSON.stringify(frame));\n\t\tconst key = frame.type === \"open\" ? `open:${frame.id}` : frame.channelId;\n\t\tconst bucket = this.buckets.get(key);\n\t\tconst perChannel = (bucket?.length ?? 0) + (this.inFlight?.key === key ? 1 : 0);\n\t\tif (\n\t\t\tbytes.length > limits.maxMessageBytes ||\n\t\t\tthis.pending.size >= limits.maxPendingOutboundMessages ||\n\t\t\tperChannel >= 32 ||\n\t\t\tthis.pendingBytes + bytes.length + this.bufferedAmount() > limits.maxPendingOutboundBytes\n\t\t) {\n\t\t\tthis.onFailure(new Error(\"Shared workspace uplink output budget exceeded; operations may have committed\"));\n\t\t\treturn Promise.reject(this.failure ?? new Error(\"Shared workspace uplink output budget exceeded\"));\n\t\t}\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tlet finished = false;\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.onFailure(new Error(\"Shared workspace uplink send timed out; operations may have committed\"));\n\t\t\t}, limits.maxTransportSendMs);\n\t\t\tconst job: Job = {\n\t\t\t\tbytes,\n\t\t\t\tkey,\n\t\t\t\tfinish: (error) => {\n\t\t\t\t\tif (finished) return;\n\t\t\t\t\tfinished = true;\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tthis.pending.delete(job);\n\t\t\t\t\tthis.pendingBytes -= bytes.length;\n\t\t\t\t\tif (this.inFlight === job) this.inFlight = undefined;\n\t\t\t\t\tif (error) reject(error);\n\t\t\t\t\telse resolve();\n\t\t\t\t},\n\t\t\t};\n\t\t\tthis.pending.add(job);\n\t\t\tthis.pendingBytes += bytes.length;\n\t\t\tif (bucket) bucket.push(job);\n\t\t\telse {\n\t\t\t\tthis.buckets.set(key, [job]);\n\t\t\t\tthis.ready.push(key);\n\t\t\t}\n\t\t\tthis.schedule();\n\t\t});\n\t}\n\n\tfail(error: Error): void {\n\t\tif (this.failure) return;\n\t\tthis.failure = error;\n\t\tfor (const job of this.pending) job.finish(error);\n\t\tthis.buckets.clear();\n\t\tthis.ready.length = 0;\n\t}\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import { type RemoteWorkspaceClientMessage, type RemoteWorkspaceServerMessage } from "./contract.ts";
2
+ export interface GuestUplinkChannel {
3
+ readonly id: string;
4
+ send(message: RemoteWorkspaceClientMessage): Promise<void>;
5
+ close(): Promise<void>;
6
+ }
7
+ export type GuestUplinkMessageHandler = (message: RemoteWorkspaceServerMessage) => void;
8
+ export type GuestUplinkFailureHandler = (error: Error) => void;
9
+ /** Owns one v2 physical socket. Loss invalidates every channel; nothing is replayed. */
10
+ export declare class GuestSharedUplink {
11
+ private readonly channels;
12
+ private readonly opens;
13
+ private failure?;
14
+ private readonly outbound;
15
+ private readonly socket;
16
+ readonly epoch: string;
17
+ /** Resolves on any terminal uplink failure (including explicit disposal); never reconnects. */
18
+ readonly whenFailed: Promise<void>;
19
+ private resolveFailure;
20
+ private constructor();
21
+ private receive;
22
+ private routeControl;
23
+ private route;
24
+ static connect(url: string, token?: string): Promise<GuestSharedUplink>;
25
+ open(root: string, onMessage: GuestUplinkMessageHandler, onFailure: GuestUplinkFailureHandler): Promise<GuestUplinkChannel>;
26
+ private send;
27
+ private fail;
28
+ dispose(): void;
29
+ }
30
+ //# sourceMappingURL=guest-shared-uplink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guest-shared-uplink.d.ts","sourceRoot":"","sources":["../../../src/core/remote-workspace-protocol/guest-shared-uplink.ts"],"names":[],"mappings":"AAGA,OAAO,EAGN,KAAK,4BAA4B,EACjC,KAAK,4BAA4B,EACjC,MAAM,eAAe,CAAC;AAQvB,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,EAAE,4BAA4B,KAAK,IAAI,CAAC;AACxF,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAa/D,wFAAwF;AACxF,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwF;IAC9G,OAAO,CAAC,OAAO,CAAC,CAAQ;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+FAA+F;IAC/F,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,CAAC,cAAc,CAAc;IAEpC,OAAO,eAkBN;IAED,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,KAAK;IAqBb,OAAa,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwE5E;IAEK,IAAI,CACT,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,yBAAyB,EACpC,SAAS,EAAE,yBAAyB,GAClC,OAAO,CAAC,kBAAkB,CAAC,CA0D7B;IAED,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,IAAI;IAmBZ,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport WebSocket from \"ws\";\nimport { VERSION } from \"../../config.ts\";\nimport {\n\tDEFAULT_REMOTE_WORKSPACE_PROTOCOL_LIMITS as limits,\n\tparseRemoteWorkspaceClientMessage,\n\ttype RemoteWorkspaceClientMessage,\n\ttype RemoteWorkspaceServerMessage,\n} from \"./contract.ts\";\nimport { type GuestOutboundFrame, GuestOutboundScheduler } from \"./guest-shared-outbound-scheduler.ts\";\nimport {\n\tparseSharedWorkspaceServerFrame,\n\tSHARED_WORKSPACE_WEBSOCKET_PROTOCOL,\n\ttype SharedWorkspaceServerFrame,\n} from \"./shared-transport.ts\";\n\nexport interface GuestUplinkChannel {\n\treadonly id: string;\n\tsend(message: RemoteWorkspaceClientMessage): Promise<void>;\n\tclose(): Promise<void>;\n}\n\nexport type GuestUplinkMessageHandler = (message: RemoteWorkspaceServerMessage) => void;\nexport type GuestUplinkFailureHandler = (error: Error) => void;\n\ntype Channel = {\n\tonMessage: GuestUplinkMessageHandler;\n\tonFailure: GuestUplinkFailureHandler;\n\tclosing: boolean;\n\tclosePromise?: Promise<void>;\n\tresolveClose?: () => void;\n\trejectClose?: (error: Error) => void;\n};\n\ntype GuestControlFrame = Extract<SharedWorkspaceServerFrame, { type: \"opened\" | \"control_error\" }>;\n\n/** Owns one v2 physical socket. Loss invalidates every channel; nothing is replayed. */\nexport class GuestSharedUplink {\n\tprivate readonly channels = new Map<string, Channel>();\n\tprivate readonly opens = new Map<string, { resolve: (id: string) => void; reject: (error: Error) => void }>();\n\tprivate failure?: Error;\n\tprivate readonly outbound: GuestOutboundScheduler;\n\tprivate readonly socket: WebSocket;\n\treadonly epoch: string;\n\t/** Resolves on any terminal uplink failure (including explicit disposal); never reconnects. */\n\treadonly whenFailed: Promise<void>;\n\tprivate resolveFailure!: () => void;\n\n\tprivate constructor(socket: WebSocket, epoch: string) {\n\t\tthis.socket = socket;\n\t\tthis.epoch = epoch;\n\t\tthis.whenFailed = new Promise<void>((resolve) => {\n\t\t\tthis.resolveFailure = resolve;\n\t\t});\n\t\tthis.outbound = new GuestOutboundScheduler(\n\t\t\t(bytes, callback) =>\n\t\t\t\tsocket.send(bytes, { binary: false, compress: false }, (error) => callback(error ?? undefined)),\n\t\t\t() => socket.bufferedAmount,\n\t\t\t(error) => this.fail(error),\n\t\t);\n\t\tsocket.on(\"message\", (data, isBinary) => this.receive(data as Buffer, isBinary));\n\t\tsocket.on(\"close\", () =>\n\t\t\tthis.fail(new Error(\"Shared workspace uplink disconnected; operations may have committed\")),\n\t\t);\n\t\tsocket.on(\"error\", () => this.fail(new Error(\"Shared workspace uplink failed; operations may have committed\")));\n\t\tif (socket.readyState !== WebSocket.OPEN) this.fail(new Error(\"Shared workspace uplink disconnected\"));\n\t}\n\n\tprivate receive(data: Buffer, isBinary: boolean): void {\n\t\ttry {\n\t\t\tif (isBinary) throw new Error(\"Unexpected binary shared workspace frame\");\n\t\t\tthis.route(parseSharedWorkspaceServerFrame(data, limits));\n\t\t} catch {\n\t\t\tthis.fail(new Error(\"Invalid or unrouteable shared workspace response\"));\n\t\t}\n\t}\n\n\tprivate routeControl(frame: GuestControlFrame): void {\n\t\tconst pending = this.opens.get(frame.id);\n\t\tif (!pending) throw new Error(\"Unknown shared workspace control ID\");\n\t\tthis.opens.delete(frame.id);\n\t\tif (frame.type === \"opened\") pending.resolve(frame.channelId);\n\t\telse pending.reject(new Error(`Shared workspace open failed: ${frame.code}`));\n\t}\n\n\tprivate route(frame: SharedWorkspaceServerFrame): void {\n\t\tif (frame.type === \"hello_ack\") throw new Error(\"Unexpected shared workspace hello acknowledgement\");\n\t\tif (frame.type === \"opened\" || frame.type === \"control_error\") {\n\t\t\tthis.routeControl(frame);\n\t\t\treturn;\n\t\t}\n\t\tconst channel = this.channels.get(frame.channelId);\n\t\tif (!channel) throw new Error(\"Unknown shared workspace channel ID\");\n\t\tif (frame.type === \"closed\") {\n\t\t\tthis.channels.delete(frame.channelId);\n\t\t\tif (frame.reason || !channel.closing) {\n\t\t\t\tconst error = new Error(frame.reason?.message ?? \"Shared workspace channel closed unexpectedly\");\n\t\t\t\tchannel.rejectClose?.(error);\n\t\t\t\tchannel.onFailure(error);\n\t\t\t} else channel.resolveClose?.();\n\t\t\treturn;\n\t\t}\n\t\t// An in-flight response can race a requested detach; deliver until closed is acknowledged.\n\t\tchannel.onMessage(frame.payload);\n\t}\n\n\tstatic async connect(url: string, token?: string): Promise<GuestSharedUplink> {\n\t\tconst endpoint = new URL(url);\n\t\tif (\n\t\t\t!([\"ws:\", \"wss:\"] as string[]).includes(endpoint.protocol) ||\n\t\t\tendpoint.username ||\n\t\t\tendpoint.password ||\n\t\t\tendpoint.hash\n\t\t) {\n\t\t\tthrow new Error(\"Invalid shared workspace endpoint\");\n\t\t}\n\t\tconst socket = new WebSocket(endpoint.toString(), SHARED_WORKSPACE_WEBSOCKET_PROTOCOL, {\n\t\t\tperMessageDeflate: false,\n\t\t\t...(token ? { headers: { authorization: `Bearer ${token}` } } : {}),\n\t\t});\n\t\ttry {\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tconst timer = setTimeout(() => reject(new Error(\"Shared workspace connection timed out\")), 5000);\n\t\t\t\tsocket.once(\"open\", () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t\tsocket.once(\"error\", () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\treject(new Error(\"Shared workspace connection failed\"));\n\t\t\t\t});\n\t\t\t});\n\t\t\tif (socket.protocol !== SHARED_WORKSPACE_WEBSOCKET_PROTOCOL) {\n\t\t\t\tthrow new Error(\"Daemon does not support grip.workspace.v2\");\n\t\t\t}\n\t\t\tconst id = randomBytes(12).toString(\"hex\");\n\t\t\tconst epoch = await new Promise<string>((resolve, reject) => {\n\t\t\t\tlet settled = false;\n\t\t\t\tconst timer = setTimeout(() => settle(undefined, new Error(\"Shared workspace hello timed out\")), 5000);\n\t\t\t\tconst settle = (value?: string, error?: Error) => {\n\t\t\t\t\tif (settled) return;\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tsocket.off(\"message\", onMessage);\n\t\t\t\t\tsocket.off(\"close\", onClose);\n\t\t\t\t\tsocket.off(\"error\", onError);\n\t\t\t\t\tif (error) reject(error);\n\t\t\t\t\telse resolve(value!);\n\t\t\t\t};\n\t\t\t\tconst onError = () => settle(undefined, new Error(\"Shared workspace hello failed\"));\n\t\t\t\tconst onClose = () => settle(undefined, new Error(\"Shared workspace hello failed\"));\n\t\t\t\tconst onMessage = (data: WebSocket.RawData, isBinary: boolean) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (isBinary) throw new Error(\"Unexpected binary frame\");\n\t\t\t\t\t\tconst frame = parseSharedWorkspaceServerFrame(Buffer.from(data as Buffer), limits);\n\t\t\t\t\t\tif (frame.type !== \"hello_ack\" || frame.id !== id) throw new Error(\"Unexpected hello response\");\n\t\t\t\t\t\tif (frame.daemonVersion !== VERSION) {\n\t\t\t\t\t\t\tsettle(undefined, new Error(\"Shared workspace daemon version mismatch\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsettle(frame.epoch);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tsettle(undefined, new Error(\"Invalid shared workspace hello response\"));\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tsocket.on(\"message\", onMessage);\n\t\t\t\tsocket.once(\"close\", onClose);\n\t\t\t\tsocket.once(\"error\", onError);\n\t\t\t\tsocket.send(JSON.stringify({ type: \"hello\", version: 2, id }), (error) => {\n\t\t\t\t\tif (error) settle(undefined, new Error(\"Shared workspace hello send failed\"));\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn new GuestSharedUplink(socket, epoch);\n\t\t} catch (error) {\n\t\t\tsocket.on(\"error\", () => undefined);\n\t\t\tsocket.terminate();\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync open(\n\t\troot: string,\n\t\tonMessage: GuestUplinkMessageHandler,\n\t\tonFailure: GuestUplinkFailureHandler,\n\t): Promise<GuestUplinkChannel> {\n\t\tif (this.failure) throw this.failure;\n\t\tif (this.channels.size + this.opens.size >= 32) throw new Error(\"Shared workspace channel limit exceeded\");\n\t\tconst id = randomBytes(12).toString(\"hex\");\n\t\tconst channelId = await new Promise<string>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(\n\t\t\t\t() => this.fail(new Error(\"Shared workspace open timed out; operations may have committed\")),\n\t\t\t\t5000,\n\t\t\t);\n\t\t\tthis.opens.set(id, {\n\t\t\t\tresolve: (openedId) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(openedId);\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\tvoid this.send({ type: \"open\", id, root }).catch((error: Error) => {\n\t\t\t\tconst pending = this.opens.get(id);\n\t\t\t\tthis.opens.delete(id);\n\t\t\t\tpending?.reject(error);\n\t\t\t});\n\t\t});\n\t\tif (this.failure) throw this.failure;\n\t\tconst channel: Channel = { onMessage, onFailure, closing: false };\n\t\tthis.channels.set(channelId, channel);\n\t\treturn {\n\t\t\tid: channelId,\n\t\t\tsend: (payload) => {\n\t\t\t\tif (channel.closing || this.channels.get(channelId) !== channel) {\n\t\t\t\t\treturn Promise.reject(new Error(\"Shared workspace channel is closed\"));\n\t\t\t\t}\n\t\t\t\tparseRemoteWorkspaceClientMessage(payload, limits);\n\t\t\t\treturn this.send({ type: \"channel\", channelId, payload });\n\t\t\t},\n\t\t\tclose: () => {\n\t\t\t\tif (channel.closePromise) return channel.closePromise;\n\t\t\t\tif (this.channels.get(channelId) !== channel) return Promise.resolve();\n\t\t\t\tchannel.closing = true;\n\t\t\t\tchannel.closePromise = new Promise<void>((resolve, reject) => {\n\t\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\t\tthis.fail(new Error(\"Shared workspace detach timed out; operations may have committed\"));\n\t\t\t\t\t}, 5000);\n\t\t\t\t\tchannel.resolveClose = () => {\n\t\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t};\n\t\t\t\t\tchannel.rejectClose = (error) => {\n\t\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t};\n\t\t\t\t\tvoid this.send({ type: \"close\", channelId }).catch(channel.rejectClose);\n\t\t\t\t});\n\t\t\t\treturn channel.closePromise;\n\t\t\t},\n\t\t};\n\t}\n\n\tprivate send(frame: GuestOutboundFrame): Promise<void> {\n\t\treturn this.outbound.send(frame);\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.failure) return;\n\t\tthis.failure = error;\n\t\tthis.outbound.fail(error);\n\t\tthis.socket.terminate();\n\t\tfor (const pending of this.opens.values()) pending.reject(error);\n\t\tthis.opens.clear();\n\t\tfor (const channel of this.channels.values()) {\n\t\t\tchannel.rejectClose?.(error);\n\t\t\ttry {\n\t\t\t\tchannel.onFailure(error);\n\t\t\t} catch {\n\t\t\t\t// Do not let one local callback block failure delivery to sibling channels.\n\t\t\t}\n\t\t}\n\t\tthis.channels.clear();\n\t\tthis.resolveFailure();\n\t}\n\n\tdispose(): void {\n\t\tthis.fail(new Error(\"Shared workspace uplink stopped; operations may have committed\"));\n\t}\n}\n"]}
@@ -0,0 +1,243 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import WebSocket from "ws";
3
+ import { VERSION } from "../../config.js";
4
+ import { DEFAULT_REMOTE_WORKSPACE_PROTOCOL_LIMITS as limits, parseRemoteWorkspaceClientMessage, } from "./contract.js";
5
+ import { GuestOutboundScheduler } from "./guest-shared-outbound-scheduler.js";
6
+ import { parseSharedWorkspaceServerFrame, SHARED_WORKSPACE_WEBSOCKET_PROTOCOL, } from "./shared-transport.js";
7
+ /** Owns one v2 physical socket. Loss invalidates every channel; nothing is replayed. */
8
+ export class GuestSharedUplink {
9
+ channels = new Map();
10
+ opens = new Map();
11
+ failure;
12
+ outbound;
13
+ socket;
14
+ epoch;
15
+ /** Resolves on any terminal uplink failure (including explicit disposal); never reconnects. */
16
+ whenFailed;
17
+ resolveFailure;
18
+ constructor(socket, epoch) {
19
+ this.socket = socket;
20
+ this.epoch = epoch;
21
+ this.whenFailed = new Promise((resolve) => {
22
+ this.resolveFailure = resolve;
23
+ });
24
+ this.outbound = new GuestOutboundScheduler((bytes, callback) => socket.send(bytes, { binary: false, compress: false }, (error) => callback(error ?? undefined)), () => socket.bufferedAmount, (error) => this.fail(error));
25
+ socket.on("message", (data, isBinary) => this.receive(data, isBinary));
26
+ socket.on("close", () => this.fail(new Error("Shared workspace uplink disconnected; operations may have committed")));
27
+ socket.on("error", () => this.fail(new Error("Shared workspace uplink failed; operations may have committed")));
28
+ if (socket.readyState !== WebSocket.OPEN)
29
+ this.fail(new Error("Shared workspace uplink disconnected"));
30
+ }
31
+ receive(data, isBinary) {
32
+ try {
33
+ if (isBinary)
34
+ throw new Error("Unexpected binary shared workspace frame");
35
+ this.route(parseSharedWorkspaceServerFrame(data, limits));
36
+ }
37
+ catch {
38
+ this.fail(new Error("Invalid or unrouteable shared workspace response"));
39
+ }
40
+ }
41
+ routeControl(frame) {
42
+ const pending = this.opens.get(frame.id);
43
+ if (!pending)
44
+ throw new Error("Unknown shared workspace control ID");
45
+ this.opens.delete(frame.id);
46
+ if (frame.type === "opened")
47
+ pending.resolve(frame.channelId);
48
+ else
49
+ pending.reject(new Error(`Shared workspace open failed: ${frame.code}`));
50
+ }
51
+ route(frame) {
52
+ if (frame.type === "hello_ack")
53
+ throw new Error("Unexpected shared workspace hello acknowledgement");
54
+ if (frame.type === "opened" || frame.type === "control_error") {
55
+ this.routeControl(frame);
56
+ return;
57
+ }
58
+ const channel = this.channels.get(frame.channelId);
59
+ if (!channel)
60
+ throw new Error("Unknown shared workspace channel ID");
61
+ if (frame.type === "closed") {
62
+ this.channels.delete(frame.channelId);
63
+ if (frame.reason || !channel.closing) {
64
+ const error = new Error(frame.reason?.message ?? "Shared workspace channel closed unexpectedly");
65
+ channel.rejectClose?.(error);
66
+ channel.onFailure(error);
67
+ }
68
+ else
69
+ channel.resolveClose?.();
70
+ return;
71
+ }
72
+ // An in-flight response can race a requested detach; deliver until closed is acknowledged.
73
+ channel.onMessage(frame.payload);
74
+ }
75
+ static async connect(url, token) {
76
+ const endpoint = new URL(url);
77
+ if (!["ws:", "wss:"].includes(endpoint.protocol) ||
78
+ endpoint.username ||
79
+ endpoint.password ||
80
+ endpoint.hash) {
81
+ throw new Error("Invalid shared workspace endpoint");
82
+ }
83
+ const socket = new WebSocket(endpoint.toString(), SHARED_WORKSPACE_WEBSOCKET_PROTOCOL, {
84
+ perMessageDeflate: false,
85
+ ...(token ? { headers: { authorization: `Bearer ${token}` } } : {}),
86
+ });
87
+ try {
88
+ await new Promise((resolve, reject) => {
89
+ const timer = setTimeout(() => reject(new Error("Shared workspace connection timed out")), 5000);
90
+ socket.once("open", () => {
91
+ clearTimeout(timer);
92
+ resolve();
93
+ });
94
+ socket.once("error", () => {
95
+ clearTimeout(timer);
96
+ reject(new Error("Shared workspace connection failed"));
97
+ });
98
+ });
99
+ if (socket.protocol !== SHARED_WORKSPACE_WEBSOCKET_PROTOCOL) {
100
+ throw new Error("Daemon does not support grip.workspace.v2");
101
+ }
102
+ const id = randomBytes(12).toString("hex");
103
+ const epoch = await new Promise((resolve, reject) => {
104
+ let settled = false;
105
+ const timer = setTimeout(() => settle(undefined, new Error("Shared workspace hello timed out")), 5000);
106
+ const settle = (value, error) => {
107
+ if (settled)
108
+ return;
109
+ settled = true;
110
+ clearTimeout(timer);
111
+ socket.off("message", onMessage);
112
+ socket.off("close", onClose);
113
+ socket.off("error", onError);
114
+ if (error)
115
+ reject(error);
116
+ else
117
+ resolve(value);
118
+ };
119
+ const onError = () => settle(undefined, new Error("Shared workspace hello failed"));
120
+ const onClose = () => settle(undefined, new Error("Shared workspace hello failed"));
121
+ const onMessage = (data, isBinary) => {
122
+ try {
123
+ if (isBinary)
124
+ throw new Error("Unexpected binary frame");
125
+ const frame = parseSharedWorkspaceServerFrame(Buffer.from(data), limits);
126
+ if (frame.type !== "hello_ack" || frame.id !== id)
127
+ throw new Error("Unexpected hello response");
128
+ if (frame.daemonVersion !== VERSION) {
129
+ settle(undefined, new Error("Shared workspace daemon version mismatch"));
130
+ return;
131
+ }
132
+ settle(frame.epoch);
133
+ }
134
+ catch {
135
+ settle(undefined, new Error("Invalid shared workspace hello response"));
136
+ }
137
+ };
138
+ socket.on("message", onMessage);
139
+ socket.once("close", onClose);
140
+ socket.once("error", onError);
141
+ socket.send(JSON.stringify({ type: "hello", version: 2, id }), (error) => {
142
+ if (error)
143
+ settle(undefined, new Error("Shared workspace hello send failed"));
144
+ });
145
+ });
146
+ return new GuestSharedUplink(socket, epoch);
147
+ }
148
+ catch (error) {
149
+ socket.on("error", () => undefined);
150
+ socket.terminate();
151
+ throw error;
152
+ }
153
+ }
154
+ async open(root, onMessage, onFailure) {
155
+ if (this.failure)
156
+ throw this.failure;
157
+ if (this.channels.size + this.opens.size >= 32)
158
+ throw new Error("Shared workspace channel limit exceeded");
159
+ const id = randomBytes(12).toString("hex");
160
+ const channelId = await new Promise((resolve, reject) => {
161
+ const timer = setTimeout(() => this.fail(new Error("Shared workspace open timed out; operations may have committed")), 5000);
162
+ this.opens.set(id, {
163
+ resolve: (openedId) => {
164
+ clearTimeout(timer);
165
+ resolve(openedId);
166
+ },
167
+ reject: (error) => {
168
+ clearTimeout(timer);
169
+ reject(error);
170
+ },
171
+ });
172
+ void this.send({ type: "open", id, root }).catch((error) => {
173
+ const pending = this.opens.get(id);
174
+ this.opens.delete(id);
175
+ pending?.reject(error);
176
+ });
177
+ });
178
+ if (this.failure)
179
+ throw this.failure;
180
+ const channel = { onMessage, onFailure, closing: false };
181
+ this.channels.set(channelId, channel);
182
+ return {
183
+ id: channelId,
184
+ send: (payload) => {
185
+ if (channel.closing || this.channels.get(channelId) !== channel) {
186
+ return Promise.reject(new Error("Shared workspace channel is closed"));
187
+ }
188
+ parseRemoteWorkspaceClientMessage(payload, limits);
189
+ return this.send({ type: "channel", channelId, payload });
190
+ },
191
+ close: () => {
192
+ if (channel.closePromise)
193
+ return channel.closePromise;
194
+ if (this.channels.get(channelId) !== channel)
195
+ return Promise.resolve();
196
+ channel.closing = true;
197
+ channel.closePromise = new Promise((resolve, reject) => {
198
+ const timer = setTimeout(() => {
199
+ this.fail(new Error("Shared workspace detach timed out; operations may have committed"));
200
+ }, 5000);
201
+ channel.resolveClose = () => {
202
+ clearTimeout(timer);
203
+ resolve();
204
+ };
205
+ channel.rejectClose = (error) => {
206
+ clearTimeout(timer);
207
+ reject(error);
208
+ };
209
+ void this.send({ type: "close", channelId }).catch(channel.rejectClose);
210
+ });
211
+ return channel.closePromise;
212
+ },
213
+ };
214
+ }
215
+ send(frame) {
216
+ return this.outbound.send(frame);
217
+ }
218
+ fail(error) {
219
+ if (this.failure)
220
+ return;
221
+ this.failure = error;
222
+ this.outbound.fail(error);
223
+ this.socket.terminate();
224
+ for (const pending of this.opens.values())
225
+ pending.reject(error);
226
+ this.opens.clear();
227
+ for (const channel of this.channels.values()) {
228
+ channel.rejectClose?.(error);
229
+ try {
230
+ channel.onFailure(error);
231
+ }
232
+ catch {
233
+ // Do not let one local callback block failure delivery to sibling channels.
234
+ }
235
+ }
236
+ this.channels.clear();
237
+ this.resolveFailure();
238
+ }
239
+ dispose() {
240
+ this.fail(new Error("Shared workspace uplink stopped; operations may have committed"));
241
+ }
242
+ }
243
+ //# sourceMappingURL=guest-shared-uplink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guest-shared-uplink.js","sourceRoot":"","sources":["../../../src/core/remote-workspace-protocol/guest-shared-uplink.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EACN,wCAAwC,IAAI,MAAM,EAClD,iCAAiC,GAGjC,MAAM,eAAe,CAAC;AACvB,OAAO,EAA2B,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACvG,OAAO,EACN,+BAA+B,EAC/B,mCAAmC,GAEnC,MAAM,uBAAuB,CAAC;AAsB/B,wFAAwF;AACxF,MAAM,OAAO,iBAAiB;IACZ,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;IACtC,KAAK,GAAG,IAAI,GAAG,EAA6E,CAAC;IACtG,OAAO,CAAS;IACP,QAAQ,CAAyB;IACjC,MAAM,CAAY;IAC1B,KAAK,CAAS;IACvB,+FAA+F;IACtF,UAAU,CAAgB;IAC3B,cAAc,CAAc;IAEpC,YAAoB,MAAiB,EAAE,KAAa,EAAE;QACrD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YAChD,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC;QAAA,CAC9B,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,IAAI,sBAAsB,CACzC,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CACnB,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC,EAChG,GAAG,EAAE,CAAC,MAAM,CAAC,cAAc,EAC3B,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAC3B,CAAC;QACF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAc,EAAE,QAAQ,CAAC,CAAC,CAAC;QACjF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CACvB,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC,CAC3F,CAAC;QACF,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC,CAAC,CAAC;QAChH,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC,CAAC;IAAA,CACvG;IAEO,OAAO,CAAC,IAAY,EAAE,QAAiB,EAAQ;QACtD,IAAI,CAAC;YACJ,IAAI,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;YAC1E,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACR,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC,CAAC;QAC1E,CAAC;IAAA,CACD;IAEO,YAAY,CAAC,KAAwB,EAAQ;QACpD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACrE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;;YACzD,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,iCAAiC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAAA,CAC9E;IAEO,KAAK,CAAC,KAAiC,EAAQ;QACtD,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW;YAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACrG,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;YAC/D,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;YACzB,OAAO;QACR,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACrE,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YACtC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBACtC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,IAAI,8CAA8C,CAAC,CAAC;gBACjG,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;gBAC7B,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YAC1B,CAAC;;gBAAM,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;YAChC,OAAO;QACR,CAAC;QACD,2FAA2F;QAC3F,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAAA,CACjC;IAED,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,GAAW,EAAE,KAAc,EAA8B;QAC7E,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,IACC,CAAE,CAAC,KAAK,EAAE,MAAM,CAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC1D,QAAQ,CAAC,QAAQ;YACjB,QAAQ,CAAC,QAAQ;YACjB,QAAQ,CAAC,IAAI,EACZ,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,mCAAmC,EAAE;YACtF,iBAAiB,EAAE,KAAK;YACxB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC,CAAC;QACH,IAAI,CAAC;YACJ,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;gBAC5C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;gBACjG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC;oBACzB,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,OAAO,EAAE,CAAC;gBAAA,CACV,CAAC,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;oBAC1B,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC,CAAC;gBAAA,CACxD,CAAC,CAAC;YAAA,CACH,CAAC,CAAC;YACH,IAAI,MAAM,CAAC,QAAQ,KAAK,mCAAmC,EAAE,CAAC;gBAC7D,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;YAC9D,CAAC;YACD,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC3C,MAAM,KAAK,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;gBAC5D,IAAI,OAAO,GAAG,KAAK,CAAC;gBACpB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;gBACvG,MAAM,MAAM,GAAG,CAAC,KAAc,EAAE,KAAa,EAAE,EAAE,CAAC;oBACjD,IAAI,OAAO;wBAAE,OAAO;oBACpB,OAAO,GAAG,IAAI,CAAC;oBACf,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;oBACjC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBAC7B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBAC7B,IAAI,KAAK;wBAAE,MAAM,CAAC,KAAK,CAAC,CAAC;;wBACpB,OAAO,CAAC,KAAM,CAAC,CAAC;gBAAA,CACrB,CAAC;gBACF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC,CAAC;gBACpF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC,CAAC;gBACpF,MAAM,SAAS,GAAG,CAAC,IAAuB,EAAE,QAAiB,EAAE,EAAE,CAAC;oBACjE,IAAI,CAAC;wBACJ,IAAI,QAAQ;4BAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;wBACzD,MAAM,KAAK,GAAG,+BAA+B,CAAC,MAAM,CAAC,IAAI,CAAC,IAAc,CAAC,EAAE,MAAM,CAAC,CAAC;wBACnF,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE;4BAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;wBAChG,IAAI,KAAK,CAAC,aAAa,KAAK,OAAO,EAAE,CAAC;4BACrC,MAAM,CAAC,SAAS,EAAE,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC,CAAC;4BACzE,OAAO;wBACR,CAAC;wBACD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBACrB,CAAC;oBAAC,MAAM,CAAC;wBACR,MAAM,CAAC,SAAS,EAAE,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC,CAAC;oBACzE,CAAC;gBAAA,CACD,CAAC;gBACF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBAChC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC9B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;oBACzE,IAAI,KAAK;wBAAE,MAAM,CAAC,SAAS,EAAE,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC,CAAC;gBAAA,CAC9E,CAAC,CAAC;YAAA,CACH,CAAC,CAAC;YACH,OAAO,IAAI,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACpC,MAAM,CAAC,SAAS,EAAE,CAAC;YACnB,MAAM,KAAK,CAAC;QACb,CAAC;IAAA,CACD;IAED,KAAK,CAAC,IAAI,CACT,IAAY,EACZ,SAAoC,EACpC,SAAoC,EACN;QAC9B,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,CAAC,OAAO,CAAC;QACrC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC3G,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;YAChE,MAAM,KAAK,GAAG,UAAU,CACvB,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC,EAC5F,IAAI,CACJ,CAAC;YACF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE;gBAClB,OAAO,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC;oBACtB,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAAA,CAClB;gBACD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;oBAClB,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,MAAM,CAAC,KAAK,CAAC,CAAC;gBAAA,CACd;aACD,CAAC,CAAC;YACH,KAAK,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE,CAAC;gBAClE,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACnC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACtB,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;YAAA,CACvB,CAAC,CAAC;QAAA,CACH,CAAC,CAAC;QACH,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,CAAC,OAAO,CAAC;QACrC,MAAM,OAAO,GAAY,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACtC,OAAO;YACN,EAAE,EAAE,SAAS;YACb,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC;gBAClB,IAAI,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,OAAO,EAAE,CAAC;oBACjE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC,CAAC;gBACxE,CAAC;gBACD,iCAAiC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;gBACnD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;YAAA,CAC1D;YACD,KAAK,EAAE,GAAG,EAAE,CAAC;gBACZ,IAAI,OAAO,CAAC,YAAY;oBAAE,OAAO,OAAO,CAAC,YAAY,CAAC;gBACtD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,OAAO;oBAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;gBACvE,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;gBACvB,OAAO,CAAC,YAAY,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;oBAC7D,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;wBAC9B,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC,CAAC;oBAAA,CACzF,EAAE,IAAI,CAAC,CAAC;oBACT,OAAO,CAAC,YAAY,GAAG,GAAG,EAAE,CAAC;wBAC5B,YAAY,CAAC,KAAK,CAAC,CAAC;wBACpB,OAAO,EAAE,CAAC;oBAAA,CACV,CAAC;oBACF,OAAO,CAAC,WAAW,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;wBAChC,YAAY,CAAC,KAAK,CAAC,CAAC;wBACpB,MAAM,CAAC,KAAK,CAAC,CAAC;oBAAA,CACd,CAAC;oBACF,KAAK,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;gBAAA,CACxE,CAAC,CAAC;gBACH,OAAO,OAAO,CAAC,YAAY,CAAC;YAAA,CAC5B;SACD,CAAC;IAAA,CACF;IAEO,IAAI,CAAC,KAAyB,EAAiB;QACtD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAAA,CACjC;IAEO,IAAI,CAAC,KAAY,EAAQ;QAChC,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACrB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACxB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;YAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC9C,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;YAC7B,IAAI,CAAC;gBACJ,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;YAC1B,CAAC;YAAC,MAAM,CAAC;gBACR,4EAA4E;YAC7E,CAAC;QACF,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,cAAc,EAAE,CAAC;IAAA,CACtB;IAED,OAAO,GAAS;QACf,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC,CAAC;IAAA,CACvF;CACD","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport WebSocket from \"ws\";\nimport { VERSION } from \"../../config.ts\";\nimport {\n\tDEFAULT_REMOTE_WORKSPACE_PROTOCOL_LIMITS as limits,\n\tparseRemoteWorkspaceClientMessage,\n\ttype RemoteWorkspaceClientMessage,\n\ttype RemoteWorkspaceServerMessage,\n} from \"./contract.ts\";\nimport { type GuestOutboundFrame, GuestOutboundScheduler } from \"./guest-shared-outbound-scheduler.ts\";\nimport {\n\tparseSharedWorkspaceServerFrame,\n\tSHARED_WORKSPACE_WEBSOCKET_PROTOCOL,\n\ttype SharedWorkspaceServerFrame,\n} from \"./shared-transport.ts\";\n\nexport interface GuestUplinkChannel {\n\treadonly id: string;\n\tsend(message: RemoteWorkspaceClientMessage): Promise<void>;\n\tclose(): Promise<void>;\n}\n\nexport type GuestUplinkMessageHandler = (message: RemoteWorkspaceServerMessage) => void;\nexport type GuestUplinkFailureHandler = (error: Error) => void;\n\ntype Channel = {\n\tonMessage: GuestUplinkMessageHandler;\n\tonFailure: GuestUplinkFailureHandler;\n\tclosing: boolean;\n\tclosePromise?: Promise<void>;\n\tresolveClose?: () => void;\n\trejectClose?: (error: Error) => void;\n};\n\ntype GuestControlFrame = Extract<SharedWorkspaceServerFrame, { type: \"opened\" | \"control_error\" }>;\n\n/** Owns one v2 physical socket. Loss invalidates every channel; nothing is replayed. */\nexport class GuestSharedUplink {\n\tprivate readonly channels = new Map<string, Channel>();\n\tprivate readonly opens = new Map<string, { resolve: (id: string) => void; reject: (error: Error) => void }>();\n\tprivate failure?: Error;\n\tprivate readonly outbound: GuestOutboundScheduler;\n\tprivate readonly socket: WebSocket;\n\treadonly epoch: string;\n\t/** Resolves on any terminal uplink failure (including explicit disposal); never reconnects. */\n\treadonly whenFailed: Promise<void>;\n\tprivate resolveFailure!: () => void;\n\n\tprivate constructor(socket: WebSocket, epoch: string) {\n\t\tthis.socket = socket;\n\t\tthis.epoch = epoch;\n\t\tthis.whenFailed = new Promise<void>((resolve) => {\n\t\t\tthis.resolveFailure = resolve;\n\t\t});\n\t\tthis.outbound = new GuestOutboundScheduler(\n\t\t\t(bytes, callback) =>\n\t\t\t\tsocket.send(bytes, { binary: false, compress: false }, (error) => callback(error ?? undefined)),\n\t\t\t() => socket.bufferedAmount,\n\t\t\t(error) => this.fail(error),\n\t\t);\n\t\tsocket.on(\"message\", (data, isBinary) => this.receive(data as Buffer, isBinary));\n\t\tsocket.on(\"close\", () =>\n\t\t\tthis.fail(new Error(\"Shared workspace uplink disconnected; operations may have committed\")),\n\t\t);\n\t\tsocket.on(\"error\", () => this.fail(new Error(\"Shared workspace uplink failed; operations may have committed\")));\n\t\tif (socket.readyState !== WebSocket.OPEN) this.fail(new Error(\"Shared workspace uplink disconnected\"));\n\t}\n\n\tprivate receive(data: Buffer, isBinary: boolean): void {\n\t\ttry {\n\t\t\tif (isBinary) throw new Error(\"Unexpected binary shared workspace frame\");\n\t\t\tthis.route(parseSharedWorkspaceServerFrame(data, limits));\n\t\t} catch {\n\t\t\tthis.fail(new Error(\"Invalid or unrouteable shared workspace response\"));\n\t\t}\n\t}\n\n\tprivate routeControl(frame: GuestControlFrame): void {\n\t\tconst pending = this.opens.get(frame.id);\n\t\tif (!pending) throw new Error(\"Unknown shared workspace control ID\");\n\t\tthis.opens.delete(frame.id);\n\t\tif (frame.type === \"opened\") pending.resolve(frame.channelId);\n\t\telse pending.reject(new Error(`Shared workspace open failed: ${frame.code}`));\n\t}\n\n\tprivate route(frame: SharedWorkspaceServerFrame): void {\n\t\tif (frame.type === \"hello_ack\") throw new Error(\"Unexpected shared workspace hello acknowledgement\");\n\t\tif (frame.type === \"opened\" || frame.type === \"control_error\") {\n\t\t\tthis.routeControl(frame);\n\t\t\treturn;\n\t\t}\n\t\tconst channel = this.channels.get(frame.channelId);\n\t\tif (!channel) throw new Error(\"Unknown shared workspace channel ID\");\n\t\tif (frame.type === \"closed\") {\n\t\t\tthis.channels.delete(frame.channelId);\n\t\t\tif (frame.reason || !channel.closing) {\n\t\t\t\tconst error = new Error(frame.reason?.message ?? \"Shared workspace channel closed unexpectedly\");\n\t\t\t\tchannel.rejectClose?.(error);\n\t\t\t\tchannel.onFailure(error);\n\t\t\t} else channel.resolveClose?.();\n\t\t\treturn;\n\t\t}\n\t\t// An in-flight response can race a requested detach; deliver until closed is acknowledged.\n\t\tchannel.onMessage(frame.payload);\n\t}\n\n\tstatic async connect(url: string, token?: string): Promise<GuestSharedUplink> {\n\t\tconst endpoint = new URL(url);\n\t\tif (\n\t\t\t!([\"ws:\", \"wss:\"] as string[]).includes(endpoint.protocol) ||\n\t\t\tendpoint.username ||\n\t\t\tendpoint.password ||\n\t\t\tendpoint.hash\n\t\t) {\n\t\t\tthrow new Error(\"Invalid shared workspace endpoint\");\n\t\t}\n\t\tconst socket = new WebSocket(endpoint.toString(), SHARED_WORKSPACE_WEBSOCKET_PROTOCOL, {\n\t\t\tperMessageDeflate: false,\n\t\t\t...(token ? { headers: { authorization: `Bearer ${token}` } } : {}),\n\t\t});\n\t\ttry {\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tconst timer = setTimeout(() => reject(new Error(\"Shared workspace connection timed out\")), 5000);\n\t\t\t\tsocket.once(\"open\", () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t\tsocket.once(\"error\", () => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\treject(new Error(\"Shared workspace connection failed\"));\n\t\t\t\t});\n\t\t\t});\n\t\t\tif (socket.protocol !== SHARED_WORKSPACE_WEBSOCKET_PROTOCOL) {\n\t\t\t\tthrow new Error(\"Daemon does not support grip.workspace.v2\");\n\t\t\t}\n\t\t\tconst id = randomBytes(12).toString(\"hex\");\n\t\t\tconst epoch = await new Promise<string>((resolve, reject) => {\n\t\t\t\tlet settled = false;\n\t\t\t\tconst timer = setTimeout(() => settle(undefined, new Error(\"Shared workspace hello timed out\")), 5000);\n\t\t\t\tconst settle = (value?: string, error?: Error) => {\n\t\t\t\t\tif (settled) return;\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tsocket.off(\"message\", onMessage);\n\t\t\t\t\tsocket.off(\"close\", onClose);\n\t\t\t\t\tsocket.off(\"error\", onError);\n\t\t\t\t\tif (error) reject(error);\n\t\t\t\t\telse resolve(value!);\n\t\t\t\t};\n\t\t\t\tconst onError = () => settle(undefined, new Error(\"Shared workspace hello failed\"));\n\t\t\t\tconst onClose = () => settle(undefined, new Error(\"Shared workspace hello failed\"));\n\t\t\t\tconst onMessage = (data: WebSocket.RawData, isBinary: boolean) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (isBinary) throw new Error(\"Unexpected binary frame\");\n\t\t\t\t\t\tconst frame = parseSharedWorkspaceServerFrame(Buffer.from(data as Buffer), limits);\n\t\t\t\t\t\tif (frame.type !== \"hello_ack\" || frame.id !== id) throw new Error(\"Unexpected hello response\");\n\t\t\t\t\t\tif (frame.daemonVersion !== VERSION) {\n\t\t\t\t\t\t\tsettle(undefined, new Error(\"Shared workspace daemon version mismatch\"));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsettle(frame.epoch);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tsettle(undefined, new Error(\"Invalid shared workspace hello response\"));\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tsocket.on(\"message\", onMessage);\n\t\t\t\tsocket.once(\"close\", onClose);\n\t\t\t\tsocket.once(\"error\", onError);\n\t\t\t\tsocket.send(JSON.stringify({ type: \"hello\", version: 2, id }), (error) => {\n\t\t\t\t\tif (error) settle(undefined, new Error(\"Shared workspace hello send failed\"));\n\t\t\t\t});\n\t\t\t});\n\t\t\treturn new GuestSharedUplink(socket, epoch);\n\t\t} catch (error) {\n\t\t\tsocket.on(\"error\", () => undefined);\n\t\t\tsocket.terminate();\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync open(\n\t\troot: string,\n\t\tonMessage: GuestUplinkMessageHandler,\n\t\tonFailure: GuestUplinkFailureHandler,\n\t): Promise<GuestUplinkChannel> {\n\t\tif (this.failure) throw this.failure;\n\t\tif (this.channels.size + this.opens.size >= 32) throw new Error(\"Shared workspace channel limit exceeded\");\n\t\tconst id = randomBytes(12).toString(\"hex\");\n\t\tconst channelId = await new Promise<string>((resolve, reject) => {\n\t\t\tconst timer = setTimeout(\n\t\t\t\t() => this.fail(new Error(\"Shared workspace open timed out; operations may have committed\")),\n\t\t\t\t5000,\n\t\t\t);\n\t\t\tthis.opens.set(id, {\n\t\t\t\tresolve: (openedId) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tresolve(openedId);\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\tvoid this.send({ type: \"open\", id, root }).catch((error: Error) => {\n\t\t\t\tconst pending = this.opens.get(id);\n\t\t\t\tthis.opens.delete(id);\n\t\t\t\tpending?.reject(error);\n\t\t\t});\n\t\t});\n\t\tif (this.failure) throw this.failure;\n\t\tconst channel: Channel = { onMessage, onFailure, closing: false };\n\t\tthis.channels.set(channelId, channel);\n\t\treturn {\n\t\t\tid: channelId,\n\t\t\tsend: (payload) => {\n\t\t\t\tif (channel.closing || this.channels.get(channelId) !== channel) {\n\t\t\t\t\treturn Promise.reject(new Error(\"Shared workspace channel is closed\"));\n\t\t\t\t}\n\t\t\t\tparseRemoteWorkspaceClientMessage(payload, limits);\n\t\t\t\treturn this.send({ type: \"channel\", channelId, payload });\n\t\t\t},\n\t\t\tclose: () => {\n\t\t\t\tif (channel.closePromise) return channel.closePromise;\n\t\t\t\tif (this.channels.get(channelId) !== channel) return Promise.resolve();\n\t\t\t\tchannel.closing = true;\n\t\t\t\tchannel.closePromise = new Promise<void>((resolve, reject) => {\n\t\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\t\tthis.fail(new Error(\"Shared workspace detach timed out; operations may have committed\"));\n\t\t\t\t\t}, 5000);\n\t\t\t\t\tchannel.resolveClose = () => {\n\t\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t};\n\t\t\t\t\tchannel.rejectClose = (error) => {\n\t\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t};\n\t\t\t\t\tvoid this.send({ type: \"close\", channelId }).catch(channel.rejectClose);\n\t\t\t\t});\n\t\t\t\treturn channel.closePromise;\n\t\t\t},\n\t\t};\n\t}\n\n\tprivate send(frame: GuestOutboundFrame): Promise<void> {\n\t\treturn this.outbound.send(frame);\n\t}\n\n\tprivate fail(error: Error): void {\n\t\tif (this.failure) return;\n\t\tthis.failure = error;\n\t\tthis.outbound.fail(error);\n\t\tthis.socket.terminate();\n\t\tfor (const pending of this.opens.values()) pending.reject(error);\n\t\tthis.opens.clear();\n\t\tfor (const channel of this.channels.values()) {\n\t\t\tchannel.rejectClose?.(error);\n\t\t\ttry {\n\t\t\t\tchannel.onFailure(error);\n\t\t\t} catch {\n\t\t\t\t// Do not let one local callback block failure delivery to sibling channels.\n\t\t\t}\n\t\t}\n\t\tthis.channels.clear();\n\t\tthis.resolveFailure();\n\t}\n\n\tdispose(): void {\n\t\tthis.fail(new Error(\"Shared workspace uplink stopped; operations may have committed\"));\n\t}\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /** Private host location for named-VM owner locks. Keep the established directory identity. */
2
+ export declare function prepareSharedLimaOwnerDirectory(baseDirectory: string): Promise<string>;
3
+ //# sourceMappingURL=lima-owner-directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lima-owner-directory.d.ts","sourceRoot":"","sources":["../../../src/core/remote-workspace-protocol/lima-owner-directory.ts"],"names":[],"mappings":"AAGA,+FAA+F;AAC/F,wBAAsB,+BAA+B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB5F","sourcesContent":["import { lstat, mkdir, realpath } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n/** Private host location for named-VM owner locks. Keep the established directory identity. */\nexport async function prepareSharedLimaOwnerDirectory(baseDirectory: string): Promise<string> {\n\tif (process.platform === \"win32\" || !process.getuid) throw new Error(\"Shared Lima owner requires Unix credentials\");\n\tconst base = await realpath(baseDirectory);\n\tconst baseInfo = await lstat(base);\n\tconst uid = process.getuid();\n\tif (\n\t\t!baseInfo.isDirectory() ||\n\t\t(baseInfo.uid !== uid && baseInfo.uid !== 0) ||\n\t\t((baseInfo.mode & 0o022) !== 0 && (baseInfo.mode & 0o1000) === 0)\n\t) {\n\t\tthrow new Error(\"Shared Lima owner base directory is not owned or sticky-safe\");\n\t}\n\t// Existing owner locks live here; changing this path could bypass a live lock.\n\tconst directory = join(base, `grip-broker-${uid}`);\n\ttry {\n\t\tawait mkdir(directory, { mode: 0o700 });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\t}\n\t// Never repair an existing directory: it might belong to another principal.\n\tconst info = await lstat(directory);\n\tif (!info.isDirectory() || info.uid !== uid || (info.mode & 0o077) !== 0) {\n\t\tthrow new Error(\"Shared Lima owner directory is not private and owned by this user\");\n\t}\n\treturn directory;\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import { lstat, mkdir, realpath } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /** Private host location for named-VM owner locks. Keep the established directory identity. */
4
+ export async function prepareSharedLimaOwnerDirectory(baseDirectory) {
5
+ if (process.platform === "win32" || !process.getuid)
6
+ throw new Error("Shared Lima owner requires Unix credentials");
7
+ const base = await realpath(baseDirectory);
8
+ const baseInfo = await lstat(base);
9
+ const uid = process.getuid();
10
+ if (!baseInfo.isDirectory() ||
11
+ (baseInfo.uid !== uid && baseInfo.uid !== 0) ||
12
+ ((baseInfo.mode & 0o022) !== 0 && (baseInfo.mode & 0o1000) === 0)) {
13
+ throw new Error("Shared Lima owner base directory is not owned or sticky-safe");
14
+ }
15
+ // Existing owner locks live here; changing this path could bypass a live lock.
16
+ const directory = join(base, `grip-broker-${uid}`);
17
+ try {
18
+ await mkdir(directory, { mode: 0o700 });
19
+ }
20
+ catch (error) {
21
+ if (error.code !== "EEXIST")
22
+ throw error;
23
+ }
24
+ // Never repair an existing directory: it might belong to another principal.
25
+ const info = await lstat(directory);
26
+ if (!info.isDirectory() || info.uid !== uid || (info.mode & 0o077) !== 0) {
27
+ throw new Error("Shared Lima owner directory is not private and owned by this user");
28
+ }
29
+ return directory;
30
+ }
31
+ //# sourceMappingURL=lima-owner-directory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lima-owner-directory.js","sourceRoot":"","sources":["../../../src/core/remote-workspace-protocol/lima-owner-directory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,+FAA+F;AAC/F,MAAM,CAAC,KAAK,UAAU,+BAA+B,CAAC,aAAqB,EAAmB;IAC7F,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACpH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAC7B,IACC,CAAC,QAAQ,CAAC,WAAW,EAAE;QACvB,CAAC,QAAQ,CAAC,GAAG,KAAK,GAAG,IAAI,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC;QAC5C,CAAC,CAAC,QAAQ,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAChE,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IACjF,CAAC;IACD,+EAA+E;IAC/E,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,eAAe,GAAG,EAAE,CAAC,CAAC;IACnD,IAAI,CAAC;QACJ,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAC;IACrE,CAAC;IACD,4EAA4E;IAC5E,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACtF,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["import { lstat, mkdir, realpath } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n/** Private host location for named-VM owner locks. Keep the established directory identity. */\nexport async function prepareSharedLimaOwnerDirectory(baseDirectory: string): Promise<string> {\n\tif (process.platform === \"win32\" || !process.getuid) throw new Error(\"Shared Lima owner requires Unix credentials\");\n\tconst base = await realpath(baseDirectory);\n\tconst baseInfo = await lstat(base);\n\tconst uid = process.getuid();\n\tif (\n\t\t!baseInfo.isDirectory() ||\n\t\t(baseInfo.uid !== uid && baseInfo.uid !== 0) ||\n\t\t((baseInfo.mode & 0o022) !== 0 && (baseInfo.mode & 0o1000) === 0)\n\t) {\n\t\tthrow new Error(\"Shared Lima owner base directory is not owned or sticky-safe\");\n\t}\n\t// Existing owner locks live here; changing this path could bypass a live lock.\n\tconst directory = join(base, `grip-broker-${uid}`);\n\ttry {\n\t\tawait mkdir(directory, { mode: 0o700 });\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\t}\n\t// Never repair an existing directory: it might belong to another principal.\n\tconst info = await lstat(directory);\n\tif (!info.isDirectory() || info.uid !== uid || (info.mode & 0o077) !== 0) {\n\t\tthrow new Error(\"Shared Lima owner directory is not private and owned by this user\");\n\t}\n\treturn directory;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ export interface SharedLimaOwnerLock {
2
+ readonly path: string;
3
+ readonly instanceName: string;
4
+ assertHeld(): Promise<void>;
5
+ release(): Promise<void>;
6
+ }
7
+ /** Exclusive owner lifetime, not a client lease. Never silently steal a stale lock. */
8
+ export declare function acquireSharedLimaOwnerLock(instanceName: string, baseDirectory?: string): Promise<SharedLimaOwnerLock>;
9
+ //# sourceMappingURL=lima-owner-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lima-owner-lock.d.ts","sourceRoot":"","sources":["../../../src/core/remote-workspace-protocol/lima-owner-lock.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAiBD,uFAAuF;AACvF,wBAAsB,0BAA0B,CAC/C,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,CAAC,CA+D9B","sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\nimport { type FileHandle, lstat, open, readFile, unlink } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { prepareSharedLimaOwnerDirectory } from \"./lima-owner-directory.ts\";\n\nexport interface SharedLimaOwnerLock {\n\treadonly path: string;\n\treadonly instanceName: string;\n\tassertHeld(): Promise<void>;\n\trelease(): Promise<void>;\n}\n\ninterface LockRecord {\n\tpid: number;\n\tnonce: string;\n}\n\nfunction isAlive(pid: number): boolean {\n\tif (!Number.isSafeInteger(pid) || pid < 1) return false;\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\n/** Exclusive owner lifetime, not a client lease. Never silently steal a stale lock. */\nexport async function acquireSharedLimaOwnerLock(\n\tinstanceName: string,\n\tbaseDirectory?: string,\n): Promise<SharedLimaOwnerLock> {\n\tif (!/^[a-z0-9][a-z0-9-]*$/.test(instanceName)) throw new Error(\"Invalid shared Lima instance name\");\n\t// The production lock must not follow TMPDIR: separate processes can otherwise\n\t// claim the same named VM through different private temporary directories.\n\t// An explicit base is reserved for isolated embedded tests.\n\tconst directory = await prepareSharedLimaOwnerDirectory(baseDirectory ?? \"/tmp\");\n\tconst digest = createHash(\"sha256\").update(instanceName).digest(\"hex\").slice(0, 32);\n\tconst path = join(directory, `lima-owner-${digest}.lock`);\n\tconst record: LockRecord = { pid: process.pid, nonce: randomBytes(16).toString(\"hex\") };\n\tlet handle: FileHandle;\n\ttry {\n\t\thandle = await open(path, \"wx\", 0o600);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\t\t// Even a dead PID is ambiguous (crash during a mutation or VM startup). Manual\n\t\t// reconciliation is required; another owner must not implicitly take over.\n\t\tlet state = \"unverifiable\";\n\t\ttry {\n\t\t\tconst info = await lstat(path);\n\t\t\tif (info.isFile() && info.uid === process.getuid?.() && (info.mode & 0o077) === 0) {\n\t\t\t\tconst existing = JSON.parse(await readFile(path, \"utf8\")) as Partial<LockRecord>;\n\t\t\t\tstate = typeof existing.pid === \"number\" && isAlive(existing.pid) ? \"occupied\" : \"stale\";\n\t\t\t}\n\t\t} catch {\n\t\t\t// A racing file change is not grounds for a new owner.\n\t\t}\n\t\tthrow new Error(`Shared Lima owner lock ${state}; inspect and reconcile before retrying`);\n\t}\n\ttry {\n\t\tawait handle.writeFile(JSON.stringify(record));\n\t\tconst identity = await handle.stat();\n\t\tlet released = false;\n\t\tconst assertHeld = async (): Promise<void> => {\n\t\t\tif (released) throw new Error(\"Shared Lima owner lock already released\");\n\t\t\tconst current = await lstat(path);\n\t\t\tif (current.dev !== identity.dev || current.ino !== identity.ino || !current.isFile()) {\n\t\t\t\tthrow new Error(\"Shared Lima owner lock identity changed; refusing operation\");\n\t\t\t}\n\t\t\tconst content = JSON.parse(await readFile(path, \"utf8\")) as Partial<LockRecord>;\n\t\t\tif (content.pid !== record.pid || content.nonce !== record.nonce) {\n\t\t\t\tthrow new Error(\"Shared Lima owner lock contents changed; refusing operation\");\n\t\t\t}\n\t\t};\n\t\treturn {\n\t\t\tpath,\n\t\t\tinstanceName,\n\t\t\tassertHeld,\n\t\t\tasync release(): Promise<void> {\n\t\t\t\tif (released) return;\n\t\t\t\ttry {\n\t\t\t\t\tawait assertHeld();\n\t\t\t\t\tawait unlink(path);\n\t\t\t\t\treleased = true;\n\t\t\t\t} finally {\n\t\t\t\t\tawait handle.close();\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tawait handle.close();\n\t\t// Do not unlink an ambiguous lock after an interrupted write.\n\t\tthrow error;\n\t}\n}\n"]}