@cjhyy/code-shell-server 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +77 -0
  2. package/dist/attachment-service.d.ts +80 -0
  3. package/dist/attachment-service.js +587 -0
  4. package/dist/bin/code-shell-serve.d.ts +2 -0
  5. package/dist/bin/code-shell-serve.js +7 -0
  6. package/dist/client-message-id.d.ts +1 -0
  7. package/dist/client-message-id.js +8 -0
  8. package/dist/image-byte-probe.d.ts +17 -0
  9. package/dist/image-byte-probe.js +330 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.js +10 -0
  12. package/dist/index.mobile-remote.d.ts +21 -0
  13. package/dist/index.mobile-remote.js +21 -0
  14. package/dist/index.serve.d.ts +3 -0
  15. package/dist/index.serve.js +3 -0
  16. package/dist/index.storage.d.ts +10 -0
  17. package/dist/index.storage.js +10 -0
  18. package/dist/index.worker.d.ts +2 -0
  19. package/dist/index.worker.js +2 -0
  20. package/dist/mobile-remote/access-passcode.d.ts +80 -0
  21. package/dist/mobile-remote/access-passcode.js +354 -0
  22. package/dist/mobile-remote/cloudflared-binary.d.ts +76 -0
  23. package/dist/mobile-remote/cloudflared-binary.js +242 -0
  24. package/dist/mobile-remote/codex-parse.d.ts +34 -0
  25. package/dist/mobile-remote/codex-parse.js +131 -0
  26. package/dist/mobile-remote/codex-room-agent.d.ts +94 -0
  27. package/dist/mobile-remote/codex-room-agent.js +213 -0
  28. package/dist/mobile-remote/mobile-attachments.d.ts +21 -0
  29. package/dist/mobile-remote/mobile-attachments.js +119 -0
  30. package/dist/mobile-remote/mobile-chat-turn.d.ts +32 -0
  31. package/dist/mobile-remote/mobile-chat-turn.js +67 -0
  32. package/dist/mobile-remote/mobile-history.d.ts +22 -0
  33. package/dist/mobile-remote/mobile-history.js +27 -0
  34. package/dist/mobile-remote/mobile-run-dispatch.d.ts +17 -0
  35. package/dist/mobile-remote/mobile-run-dispatch.js +56 -0
  36. package/dist/mobile-remote/mobile-static.d.ts +43 -0
  37. package/dist/mobile-remote/mobile-static.js +154 -0
  38. package/dist/mobile-remote/mobile-upload-service.d.ts +57 -0
  39. package/dist/mobile-remote/mobile-upload-service.js +370 -0
  40. package/dist/mobile-remote/pairing.d.ts +8 -0
  41. package/dist/mobile-remote/pairing.js +23 -0
  42. package/dist/mobile-remote/path-bins.d.ts +7 -0
  43. package/dist/mobile-remote/path-bins.js +16 -0
  44. package/dist/mobile-remote/pending-approvals.d.ts +14 -0
  45. package/dist/mobile-remote/pending-approvals.js +54 -0
  46. package/dist/mobile-remote/remote-host-manager.d.ts +117 -0
  47. package/dist/mobile-remote/remote-host-manager.js +345 -0
  48. package/dist/mobile-remote/resident-agent.d.ts +98 -0
  49. package/dist/mobile-remote/resident-agent.js +216 -0
  50. package/dist/mobile-remote/room-manager.d.ts +286 -0
  51. package/dist/mobile-remote/room-manager.js +765 -0
  52. package/dist/mobile-remote/trusted-device-store.d.ts +19 -0
  53. package/dist/mobile-remote/trusted-device-store.js +118 -0
  54. package/dist/mobile-remote/tunnel-manager.d.ts +105 -0
  55. package/dist/mobile-remote/tunnel-manager.js +375 -0
  56. package/dist/mobile-remote/types.d.ts +1 -0
  57. package/dist/mobile-remote/types.js +1 -0
  58. package/dist/mobile-remote/viewer-identity.d.ts +5 -0
  59. package/dist/mobile-remote/viewer-identity.js +5 -0
  60. package/dist/serve/cli.d.ts +15 -0
  61. package/dist/serve/cli.js +115 -0
  62. package/dist/serve/headless-server.d.ts +40 -0
  63. package/dist/serve/headless-server.js +276 -0
  64. package/dist/sessions-service.d.ts +73 -0
  65. package/dist/sessions-service.js +303 -0
  66. package/dist/static-files.d.ts +1 -0
  67. package/dist/static-files.js +25 -0
  68. package/dist/worker-bridge-core.d.ts +154 -0
  69. package/dist/worker-bridge-core.js +315 -0
  70. package/package.json +63 -0
@@ -0,0 +1,67 @@
1
+ import { markAttachmentsSent } from "../attachment-service.js";
2
+ import { stablePromptHash } from "../client-message-id.js";
3
+ import { materializeMobileAttachments } from "./mobile-attachments.js";
4
+ import { injectMobileRunAndAwaitAcceptance } from "./mobile-run-dispatch.js";
5
+ async function settleClaims(uploads, deviceId, claims, action) {
6
+ await Promise.allSettled(claims.map((claim) => uploads[action](deviceId, claim.uploadId, claim.claimId)));
7
+ }
8
+ /** Transaction boundary for mobile chat: workspace → canonical stage → worker ack → finalize. */
9
+ export async function dispatchMobileChatTurn(input) {
10
+ let cwd;
11
+ try {
12
+ cwd = await input.resolveWorkspace(input.sessionId, input.fallbackCwd);
13
+ }
14
+ catch (error) {
15
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
16
+ }
17
+ let materialized;
18
+ try {
19
+ materialized = await materializeMobileAttachments({
20
+ deviceId: input.deviceId,
21
+ cwd,
22
+ sessionId: input.sessionId,
23
+ attachments: input.attachments,
24
+ uploads: input.uploads,
25
+ });
26
+ }
27
+ catch (error) {
28
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
29
+ }
30
+ const text = input.text.trim();
31
+ if (!text && materialized.metas.length === 0) {
32
+ await settleClaims(input.uploads, input.deviceId, materialized.claims, "release");
33
+ return { ok: false, message: "消息必须包含文字或图片" };
34
+ }
35
+ const attachmentHash = materialized.metas.map((meta) => meta.sha256).join(":");
36
+ const suppliedClientMessageId = input.clientMessageId?.trim();
37
+ if (suppliedClientMessageId && suppliedClientMessageId.length > 200) {
38
+ await settleClaims(input.uploads, input.deviceId, materialized.claims, "release");
39
+ return { ok: false, message: "clientMessageId is too long" };
40
+ }
41
+ const clientMessageId = suppliedClientMessageId ||
42
+ `mobile:${input.sessionId}:${input.runId}:${stablePromptHash(`${text}\0${attachmentHash}`)}`;
43
+ const acceptance = await injectMobileRunAndAwaitAcceptance(input.bridge, {
44
+ id: input.runId,
45
+ params: {
46
+ task: text,
47
+ cwd,
48
+ sessionId: input.sessionId,
49
+ clientMessageId,
50
+ attachments: materialized.metas,
51
+ ...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
52
+ },
53
+ });
54
+ if (!acceptance.ok) {
55
+ await settleClaims(input.uploads, input.deviceId, materialized.claims, "release");
56
+ return { ok: false, message: acceptance.message };
57
+ }
58
+ await (input.markSent ?? markAttachmentsSent)(cwd, input.sessionId, materialized.metas).catch(() => undefined);
59
+ await settleClaims(input.uploads, input.deviceId, materialized.claims, "finalize");
60
+ return {
61
+ ok: true,
62
+ cwd,
63
+ clientMessageId,
64
+ metas: materialized.metas,
65
+ summaries: materialized.summaries,
66
+ };
67
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Build a session's history as a flat list of events the phone's streamReducer
3
+ * understands. We REUSE the desktop's transcript reader (transcriptToFoldItems
4
+ * via its getSessionTranscript) so there is one transcript parser, then project
5
+ * its FoldItems into reducer events:
6
+ * - { kind: "stream", event } → the raw StreamEvent (reducer eats it directly)
7
+ * - { kind: "user", text } → a synthetic { type: "user_message", text }
8
+ *
9
+ * The phone feeds these through the SAME reducer used for the live stream, so
10
+ * replay and live are identical (design §4).
11
+ *
12
+ * The transcript reader itself stays in the desktop package (it depends on
13
+ * renderer FoldItem types), so it is injected here instead of imported.
14
+ */
15
+ /** Structural projection of the desktop's FoldItem — only what we consume. */
16
+ export interface TranscriptHistoryItem {
17
+ kind: string;
18
+ text?: unknown;
19
+ event?: unknown;
20
+ }
21
+ export type SessionTranscriptReader = (sessionId: string) => Promise<readonly TranscriptHistoryItem[]>;
22
+ export declare function buildSessionHistory(sessionId: string, getTranscript: SessionTranscriptReader): Promise<unknown[]>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Build a session's history as a flat list of events the phone's streamReducer
3
+ * understands. We REUSE the desktop's transcript reader (transcriptToFoldItems
4
+ * via its getSessionTranscript) so there is one transcript parser, then project
5
+ * its FoldItems into reducer events:
6
+ * - { kind: "stream", event } → the raw StreamEvent (reducer eats it directly)
7
+ * - { kind: "user", text } → a synthetic { type: "user_message", text }
8
+ *
9
+ * The phone feeds these through the SAME reducer used for the live stream, so
10
+ * replay and live are identical (design §4).
11
+ *
12
+ * The transcript reader itself stays in the desktop package (it depends on
13
+ * renderer FoldItem types), so it is injected here instead of imported.
14
+ */
15
+ export async function buildSessionHistory(sessionId, getTranscript) {
16
+ const items = await getTranscript(sessionId);
17
+ const events = [];
18
+ for (const item of items) {
19
+ if (item.kind === "user") {
20
+ events.push({ type: "user_message", text: item.text });
21
+ }
22
+ else if (item.kind === "stream") {
23
+ events.push(item.event);
24
+ }
25
+ }
26
+ return events;
27
+ }
@@ -0,0 +1,17 @@
1
+ export interface MobileRunBridge {
2
+ subscribeOutbound(listener: (line: string) => void): () => void;
3
+ injectWorkerMessage(line: string): void;
4
+ }
5
+ export interface MobileRunRequest {
6
+ id: string;
7
+ params: Record<string, unknown>;
8
+ }
9
+ export type MobileRunAcceptance = {
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ message: string;
14
+ code?: number;
15
+ };
16
+ /** Inject agent/run and wait for its request-correlated validation/queue acknowledgement. */
17
+ export declare function injectMobileRunAndAwaitAcceptance(bridge: MobileRunBridge, request: MobileRunRequest, timeoutMs?: number): Promise<MobileRunAcceptance>;
@@ -0,0 +1,56 @@
1
+ /** Inject agent/run and wait for its request-correlated validation/queue acknowledgement. */
2
+ export function injectMobileRunAndAwaitAcceptance(bridge, request, timeoutMs = 5_000) {
3
+ return new Promise((resolve) => {
4
+ let settled = false;
5
+ let timer;
6
+ const finish = (result) => {
7
+ if (settled)
8
+ return;
9
+ settled = true;
10
+ if (timer)
11
+ clearTimeout(timer);
12
+ unsubscribe();
13
+ resolve(result);
14
+ };
15
+ const unsubscribe = bridge.subscribeOutbound((line) => {
16
+ try {
17
+ const message = JSON.parse(line);
18
+ if (message.method === "agent/runAccepted" && message.params?.requestId === request.id) {
19
+ finish({ ok: true });
20
+ return;
21
+ }
22
+ if (message.id !== request.id)
23
+ return;
24
+ if (message.error) {
25
+ finish({
26
+ ok: false,
27
+ message: typeof message.error.message === "string"
28
+ ? message.error.message
29
+ : "worker rejected the run",
30
+ ...(typeof message.error.code === "number" ? { code: message.error.code } : {}),
31
+ });
32
+ }
33
+ else if ("result" in message) {
34
+ // Compatibility fallback if an older/embedded worker completes before
35
+ // emitting the explicit acknowledgement.
36
+ finish({ ok: true });
37
+ }
38
+ }
39
+ catch {
40
+ // Not JSON or not part of this request.
41
+ }
42
+ });
43
+ timer = setTimeout(() => finish({ ok: false, message: "worker did not acknowledge the run" }), timeoutMs);
44
+ try {
45
+ bridge.injectWorkerMessage(JSON.stringify({
46
+ jsonrpc: "2.0",
47
+ id: request.id,
48
+ method: "agent/run",
49
+ params: request.params,
50
+ }));
51
+ }
52
+ catch (error) {
53
+ finish({ ok: false, message: error instanceof Error ? error.message : String(error) });
54
+ }
55
+ });
56
+ }
@@ -0,0 +1,43 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ /**
3
+ * Serves the built mobile web app (out/mobile) as static assets for the
4
+ * `/mobile` route family, replacing the old inline `mobileRemoteHtml()` string.
5
+ *
6
+ * Two modes:
7
+ * - prod: read files from `out/mobile` (resolved relative to the bundled main).
8
+ * - dev: proxy to the mobile vite dev server (MOBILE_DEV_URL) for HMR.
9
+ *
10
+ * The app is built/served with vite base "/mobile/" (vite.mobile.config.ts), so
11
+ * BOTH the built bundle's asset URLs and vite's dev HMR/module URLs live under
12
+ * the `/mobile` prefix — a single prefix route serves prod and dev, with no
13
+ * root-level asset shim or whole-server dev proxy.
14
+ *
15
+ * Security (design §5): only files INSIDE out/mobile are served. The request
16
+ * path is normalized and re-resolved; anything that escapes the root → 404.
17
+ * This closes the path-traversal hole the beta1 sweep fixed elsewhere.
18
+ */
19
+ /** Strip `/mobile` (and an optional trailing slash) off the request URL, drop
20
+ * any query/hash, and return the asset sub-path. `/mobile` → "", `/mobile/` →
21
+ * "", `/mobile/assets/x.js` → "assets/x.js". */
22
+ export declare function mobileAssetPath(reqUrl: string): string;
23
+ /** If `reqUrl` is the bare `/mobile` entry (optionally with a query/hash but no
24
+ * trailing slash), return the canonical `/mobile/`-prefixed target (query/hash
25
+ * preserved) for a redirect. Returns null for `/mobile/` and any sub-path,
26
+ * which need no redirect. */
27
+ export declare function mobileEntryRedirect(reqUrl: string): string | null;
28
+ /** Resolve an asset sub-path to an absolute file inside `root`, or null if it
29
+ * would escape the root (traversal) or does not resolve to a regular file. An
30
+ * empty sub-path resolves to index.html (SPA entry). */
31
+ export declare function resolveSafe(root: string, subPath: string): string | null;
32
+ export interface MobileStaticOptions {
33
+ /** Absolute path to the built mobile app (out/mobile). */
34
+ rootDir: string;
35
+ /** When set, dev mode: proxy /mobile/* to this base URL instead of disk. */
36
+ devUrl?: string;
37
+ }
38
+ /**
39
+ * Handle a `/mobile`-prefixed request. Returns true if it handled the response
40
+ * (wrote a status), false if the caller should fall through (it never does for
41
+ * /mobile — callers check the prefix first, so this always handles).
42
+ */
43
+ export declare function serveMobile(req: IncomingMessage, res: ServerResponse, opts: MobileStaticOptions): void;
@@ -0,0 +1,154 @@
1
+ import { createReadStream, existsSync, statSync } from "node:fs";
2
+ import { join, normalize, resolve, sep, extname } from "node:path";
3
+ import { request as httpRequest } from "node:http";
4
+ import { contentTypeFor } from "../static-files.js";
5
+ /**
6
+ * Serves the built mobile web app (out/mobile) as static assets for the
7
+ * `/mobile` route family, replacing the old inline `mobileRemoteHtml()` string.
8
+ *
9
+ * Two modes:
10
+ * - prod: read files from `out/mobile` (resolved relative to the bundled main).
11
+ * - dev: proxy to the mobile vite dev server (MOBILE_DEV_URL) for HMR.
12
+ *
13
+ * The app is built/served with vite base "/mobile/" (vite.mobile.config.ts), so
14
+ * BOTH the built bundle's asset URLs and vite's dev HMR/module URLs live under
15
+ * the `/mobile` prefix — a single prefix route serves prod and dev, with no
16
+ * root-level asset shim or whole-server dev proxy.
17
+ *
18
+ * Security (design §5): only files INSIDE out/mobile are served. The request
19
+ * path is normalized and re-resolved; anything that escapes the root → 404.
20
+ * This closes the path-traversal hole the beta1 sweep fixed elsewhere.
21
+ */
22
+ /** Strip `/mobile` (and an optional trailing slash) off the request URL, drop
23
+ * any query/hash, and return the asset sub-path. `/mobile` → "", `/mobile/` →
24
+ * "", `/mobile/assets/x.js` → "assets/x.js". */
25
+ export function mobileAssetPath(reqUrl) {
26
+ const noQuery = reqUrl.split("?")[0].split("#")[0];
27
+ let rest = noQuery.slice("/mobile".length); // reqUrl always starts with /mobile here
28
+ if (rest.startsWith("/"))
29
+ rest = rest.slice(1);
30
+ return rest;
31
+ }
32
+ /** If `reqUrl` is the bare `/mobile` entry (optionally with a query/hash but no
33
+ * trailing slash), return the canonical `/mobile/`-prefixed target (query/hash
34
+ * preserved) for a redirect. Returns null for `/mobile/` and any sub-path,
35
+ * which need no redirect. */
36
+ export function mobileEntryRedirect(reqUrl) {
37
+ const qIdx = reqUrl.search(/[?#]/);
38
+ const path = qIdx === -1 ? reqUrl : reqUrl.slice(0, qIdx);
39
+ const suffix = qIdx === -1 ? "" : reqUrl.slice(qIdx);
40
+ if (path === "/mobile")
41
+ return "/mobile/" + suffix;
42
+ return null;
43
+ }
44
+ /** Resolve an asset sub-path to an absolute file inside `root`, or null if it
45
+ * would escape the root (traversal) or does not resolve to a regular file. An
46
+ * empty sub-path resolves to index.html (SPA entry). */
47
+ export function resolveSafe(root, subPath) {
48
+ const rootResolved = resolve(root);
49
+ const rel = normalize(subPath || "index.html");
50
+ // Reject absolute paths and any normalized path still containing `..`.
51
+ if (rel.startsWith("..") || rel.includes(`..${sep}`) || rel.startsWith(sep)) {
52
+ return null;
53
+ }
54
+ const full = resolve(join(rootResolved, rel));
55
+ if (full !== rootResolved && !full.startsWith(rootResolved + sep))
56
+ return null;
57
+ if (!existsSync(full) || !statSync(full).isFile())
58
+ return null;
59
+ return full;
60
+ }
61
+ /** True when the request is a browser navigation (accepts text/html), used to
62
+ * decide whether a path that doesn't resolve to a file should fall back to the
63
+ * SPA shell. Asset/module fetches send `*\/*` or a specific type and must NOT
64
+ * fall back (so a missing asset 404s instead of returning HTML). */
65
+ function acceptsHtml(req) {
66
+ const accept = req.headers.accept;
67
+ return typeof accept === "string" && accept.includes("text/html");
68
+ }
69
+ /**
70
+ * Handle a `/mobile`-prefixed request. Returns true if it handled the response
71
+ * (wrote a status), false if the caller should fall through (it never does for
72
+ * /mobile — callers check the prefix first, so this always handles).
73
+ */
74
+ export function serveMobile(req, res, opts) {
75
+ // Canonicalize the entry URL to a trailing slash. The pairing URL is
76
+ // `/mobile?pairing=...` (no slash); without the slash the served HTML's base
77
+ // is `/`, so a relative ref would escape the routed prefix. Redirect the bare
78
+ // entry → `/mobile/` (preserving the query) so the app always loads under
79
+ // `/mobile/`. Sub-paths (/mobile/assets/*) are left untouched.
80
+ const redirect = mobileEntryRedirect(req.url ?? "/mobile");
81
+ if (redirect) {
82
+ res.writeHead(308, { location: redirect });
83
+ res.end();
84
+ return;
85
+ }
86
+ if (opts.devUrl) {
87
+ proxyToDev(req, res, opts.devUrl);
88
+ return;
89
+ }
90
+ const sub = mobileAssetPath(req.url ?? "/mobile");
91
+ let file = resolveSafe(opts.rootDir, sub);
92
+ // SPA fallback: a navigation to an unknown client route → index.html so the
93
+ // React router can take over. Gate on the request ACCEPTing html (the browser
94
+ // sends `Accept: text/html` for navigations, `*/*` or a specific type for
95
+ // asset/module fetches), NOT on the absence of a file extension — a route
96
+ // segment containing a dot (e.g. a version or dotted id) must still fall back,
97
+ // and a genuinely-missing .js/.css must still 404 (returning HTML for a module
98
+ // request would break module loading).
99
+ if (!file && sub && acceptsHtml(req)) {
100
+ file = resolveSafe(opts.rootDir, "index.html");
101
+ }
102
+ if (!file) {
103
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
104
+ res.end("not found");
105
+ return;
106
+ }
107
+ const type = contentTypeFor(extname(file));
108
+ // Hashed bundles under /assets are content-addressed → cache forever. The
109
+ // entry HTML (index.html, served for "" or the SPA fallback) must NOT be
110
+ // cached: after an app upgrade ships a new bundle with new hashed filenames, a
111
+ // stale index.html would reference deleted assets → 404 → blank page until a
112
+ // hard refresh.
113
+ const isHtml = type.startsWith("text/html");
114
+ res.writeHead(200, {
115
+ "content-type": type,
116
+ "cache-control": isHtml
117
+ ? "no-store"
118
+ : sub.startsWith("assets/")
119
+ ? "public, max-age=31536000, immutable"
120
+ : "no-cache",
121
+ });
122
+ createReadStream(file).pipe(res);
123
+ }
124
+ /** Dev-only: forward the request to the mobile vite dev server. The mobile vite
125
+ * config sets base "/mobile/", so vite serves its HMR client, module graph, and
126
+ * asset URLs UNDER that prefix (/mobile/@vite/client, /mobile/src/main.tsx). We
127
+ * therefore forward the request path UNCHANGED — stripping /mobile would ask
128
+ * vite for /@vite/client, which a base-"/mobile/" server 404s, breaking HMR so
129
+ * the phone never live-updates. The /mobile prefix belongs to vite here (its
130
+ * base), not to us — only prod static serving strips it (files live flat under
131
+ * out/mobile). */
132
+ function proxyToDev(req, res, devUrl) {
133
+ const target = new URL(devUrl);
134
+ const proxyReq = httpRequest({
135
+ hostname: target.hostname,
136
+ port: target.port,
137
+ method: req.method,
138
+ path: req.url ?? "/mobile/",
139
+ headers: { ...req.headers, host: target.host },
140
+ }, (proxyRes) => {
141
+ // Never let a phone cache the DEV page/assets. Otherwise a stale static
142
+ // build's HTML (hash-named assets) survives in the browser cache and its
143
+ // asset requests hit the vite dev server, which has no hash files → 404.
144
+ // no-store on every dev response keeps the phone in lockstep with vite.
145
+ const headers = { ...proxyRes.headers, "cache-control": "no-store" };
146
+ res.writeHead(proxyRes.statusCode ?? 502, headers);
147
+ proxyRes.pipe(res);
148
+ });
149
+ proxyReq.on("error", () => {
150
+ res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
151
+ res.end("mobile dev server unavailable");
152
+ });
153
+ req.pipe(proxyReq);
154
+ }
@@ -0,0 +1,57 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { MobileImageBase } from "./types.js";
3
+ export declare const MAX_MOBILE_IMAGE_BYTES: number;
4
+ export declare const MOBILE_UPLOAD_TTL_MS: number;
5
+ export declare const MAX_MOBILE_UPLOAD_TICKETS_PER_DEVICE = 16;
6
+ export declare const MAX_MOBILE_UPLOAD_CLAIM_ATTEMPTS = 3;
7
+ export interface MobileUploadServiceOptions {
8
+ rootDir: string;
9
+ now?: () => number;
10
+ cleanupIntervalMs?: number;
11
+ uploadTtlMs?: number;
12
+ }
13
+ export interface ClaimedMobileUpload extends MobileImageBase {
14
+ uploadId: string;
15
+ claimId: string;
16
+ path: string;
17
+ sha256: string;
18
+ }
19
+ /** One-time, device-bound HTTP upload tickets backed by a temporary spool. */
20
+ export declare class MobileUploadService {
21
+ private readonly opts;
22
+ private readonly uploads;
23
+ private readonly activeTransfers;
24
+ private readonly now;
25
+ private readonly uploadTtlMs;
26
+ private readonly startupCleanup;
27
+ private readonly timer?;
28
+ private disposed;
29
+ private disposePromise?;
30
+ constructor(opts: MobileUploadServiceOptions);
31
+ /** Wait until startup orphan recovery has completed. */
32
+ ready(): Promise<void>;
33
+ begin(deviceId: string, metadata: Omit<MobileImageBase, "mime"> & {
34
+ mime: string;
35
+ }): {
36
+ clientId: string;
37
+ uploadId: string;
38
+ putUrl: string;
39
+ expiresAt: number;
40
+ };
41
+ acceptPut(uploadId: string, req: IncomingMessage, res: ServerResponse): Promise<void>;
42
+ /** Atomically move a ready upload into a request-owned lease. */
43
+ claim(deviceId: string, uploadId: string): ClaimedMobileUpload;
44
+ /** Release a failed staging/dispatch attempt for a bounded retry. */
45
+ release(deviceId: string, uploadId: string, claimId: string): Promise<void>;
46
+ /** Finalize a successful turn; only the active claim owner may consume it. */
47
+ finalize(deviceId: string, uploadId: string, claimId: string): Promise<void>;
48
+ cleanupExpired(): Promise<void>;
49
+ /** Abort all in-flight transfers and invalidate every outstanding ticket. */
50
+ cancelActiveTransfers(): Promise<void>;
51
+ /** Fully await timer shutdown, active aborts, and every spool unlink. */
52
+ dispose(): Promise<void>;
53
+ private expire;
54
+ private cleanupRecordFiles;
55
+ private cleanupOrphanSpools;
56
+ private assertClaimOwner;
57
+ }