@khorsheed/dsh-mobile 0.1.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +132 -0
  4. package/README.zh.md +132 -0
  5. package/cordis.patch.yml +3 -0
  6. package/examples/https-ingress.mjs +84 -0
  7. package/examples/safari-stream-compat.mjs +98 -0
  8. package/lib/client.js +6138 -0
  9. package/lib/index.js +172 -0
  10. package/lib/invariant.js +20 -0
  11. package/lib/types/client/ConnectPhone.d.ts +9 -0
  12. package/lib/types/client/ConnectPhone.js +83 -0
  13. package/lib/types/client/DirectoryFlow.d.ts +14 -0
  14. package/lib/types/client/DirectoryFlow.js +75 -0
  15. package/lib/types/client/MobileChrome.d.ts +15 -0
  16. package/lib/types/client/MobileChrome.js +124 -0
  17. package/lib/types/client/MobileIcon.d.ts +6 -0
  18. package/lib/types/client/MobileIcon.js +16 -0
  19. package/lib/types/client/MobileLibrary.d.ts +13 -0
  20. package/lib/types/client/MobileLibrary.js +73 -0
  21. package/lib/types/client/MobileQueue.d.ts +9 -0
  22. package/lib/types/client/MobileQueue.js +90 -0
  23. package/lib/types/client/MobileRooms.d.ts +13 -0
  24. package/lib/types/client/MobileRooms.js +80 -0
  25. package/lib/types/client/MobileSeats.d.ts +4 -0
  26. package/lib/types/client/MobileSeats.js +5 -0
  27. package/lib/types/client/MobileTools.d.ts +7 -0
  28. package/lib/types/client/MobileTools.js +34 -0
  29. package/lib/types/client/SessionRename.d.ts +10 -0
  30. package/lib/types/client/SessionRename.js +43 -0
  31. package/lib/types/client/SubmissionFocus.d.ts +26 -0
  32. package/lib/types/client/SubmissionFocus.js +74 -0
  33. package/lib/types/client/composerActions.d.ts +24 -0
  34. package/lib/types/client/composerActions.js +66 -0
  35. package/lib/types/client/composerFocus.d.ts +15 -0
  36. package/lib/types/client/composerFocus.js +41 -0
  37. package/lib/types/client/directory.d.ts +17 -0
  38. package/lib/types/client/directory.js +54 -0
  39. package/lib/types/client/foreground.d.ts +4 -0
  40. package/lib/types/client/foreground.js +47 -0
  41. package/lib/types/client/gestures.d.ts +4 -0
  42. package/lib/types/client/gestures.js +118 -0
  43. package/lib/types/client/index.d.ts +20 -0
  44. package/lib/types/client/index.js +144 -0
  45. package/lib/types/client/locales.d.ts +145 -0
  46. package/lib/types/client/locales.js +80 -0
  47. package/lib/types/client/messageMenu.d.ts +19 -0
  48. package/lib/types/client/messageMenu.js +99 -0
  49. package/lib/types/client/native.d.ts +6 -0
  50. package/lib/types/client/native.js +15 -0
  51. package/lib/types/client/navigation.d.ts +41 -0
  52. package/lib/types/client/navigation.js +61 -0
  53. package/lib/types/client/pickers.d.ts +17 -0
  54. package/lib/types/client/pickers.js +109 -0
  55. package/lib/types/client/presentation.d.ts +38 -0
  56. package/lib/types/client/presentation.js +146 -0
  57. package/lib/types/client/queue.d.ts +47 -0
  58. package/lib/types/client/queue.js +26 -0
  59. package/lib/types/client/roomActions.d.ts +6 -0
  60. package/lib/types/client/roomActions.js +41 -0
  61. package/lib/types/client/rooms.d.ts +44 -0
  62. package/lib/types/client/rooms.js +75 -0
  63. package/lib/types/client/styles.d.ts +4 -0
  64. package/lib/types/client/styles.js +434 -0
  65. package/lib/types/client/surface.d.ts +17 -0
  66. package/lib/types/client/surface.js +112 -0
  67. package/lib/types/client/viewport.d.ts +13 -0
  68. package/lib/types/client/viewport.js +42 -0
  69. package/lib/types/connect.d.ts +7 -0
  70. package/lib/types/connect.js +61 -0
  71. package/lib/types/directories.d.ts +5 -0
  72. package/lib/types/directories.js +39 -0
  73. package/lib/types/index.d.ts +9 -0
  74. package/lib/types/index.js +21 -0
  75. package/lib/types/invariant.d.ts +16 -0
  76. package/lib/types/invariant.js +17 -0
  77. package/lib/types/protocol.d.ts +30 -0
  78. package/lib/types/protocol.js +17 -0
  79. package/locale/en.json +1 -0
  80. package/locale/zh.json +1 -0
  81. package/package.json +102 -0
@@ -0,0 +1,42 @@
1
+ /** Geometry only: keep the official composer mounted and reserve its measured height. */
2
+ export class MobileViewport {
3
+ win;
4
+ seat;
5
+ root;
6
+ observer;
7
+ theme = '';
8
+ constructor(win) {
9
+ this.win = win;
10
+ if (typeof ResizeObserver !== 'undefined')
11
+ this.observer = new ResizeObserver(() => this.measure());
12
+ }
13
+ sync(frame) {
14
+ const root = frame?.querySelector('[data-slot="main.conversation"] > [data-phase]') ?? undefined;
15
+ const seat = root?.querySelector('[data-composer-seat]') ?? undefined;
16
+ if (seat !== this.seat || root !== this.root) {
17
+ this.observer?.disconnect();
18
+ this.root?.style.removeProperty('--mobile-composer-height');
19
+ this.root = root;
20
+ this.seat = seat;
21
+ if (seat)
22
+ this.observer?.observe(seat);
23
+ }
24
+ this.measure();
25
+ if (!frame)
26
+ return;
27
+ // Resolve CSS aliases through a color property, not a guessed light/dark palette.
28
+ const background = this.win.getComputedStyle(frame).backgroundColor;
29
+ if (background !== this.theme && /^rgba?\([\d.,\s]+\)$/.test(background)) {
30
+ this.theme = background;
31
+ this.win.webkit?.messageHandlers?.dshMobile?.postMessage({ type: 'appearance', bridgeVersion: 1, background: background.match(/[\d.]+/g)?.slice(0, 3).map(Number) });
32
+ }
33
+ }
34
+ measure() {
35
+ if (!this.seat || !this.root)
36
+ return;
37
+ const height = `${Math.ceil(this.seat.getBoundingClientRect().height)}px`;
38
+ if (this.root.style.getPropertyValue('--mobile-composer-height') !== height)
39
+ this.root.style.setProperty('--mobile-composer-height', height);
40
+ }
41
+ dispose() { this.observer?.disconnect(); this.root?.style.removeProperty('--mobile-composer-height'); this.root = undefined; this.seat = undefined; }
42
+ }
@@ -0,0 +1,7 @@
1
+ import type { HostConnectionHandle } from '@deepseek-ai/dsh-client-connection';
2
+ import type { MobileConnectInfo } from './protocol.ts';
3
+ /** Deployment-owned destination only: a browser cannot choose where a bearer is sent. */
4
+ export declare function connectInfo(connection: HostConnectionHandle, configured?: string): MobileConnectInfo;
5
+ /** Called only behind the official authenticated Fetch registry. */
6
+ export declare function connectResponse(connection: HostConnectionHandle, configured?: string): (request: Request) => Promise<Response>;
7
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1,61 @@
1
+ const headers = { 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' };
2
+ /** Deployment-owned destination only: a browser cannot choose where a bearer is sent. */
3
+ export function connectInfo(connection, configured) {
4
+ if (!configured)
5
+ return { state: 'not-configured', origin: null };
6
+ let url;
7
+ try {
8
+ url = new URL(configured);
9
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/'
10
+ || ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
11
+ throw new Error('invalid');
12
+ }
13
+ catch {
14
+ return { state: 'invalid-origin', origin: null };
15
+ }
16
+ if (typeof connection.authenticatedUrl !== 'function' || typeof connection.requestRejection !== 'function') {
17
+ return { state: 'unsupported', origin: url.origin };
18
+ }
19
+ // Probe the public Host/Origin fence without credentials. 401 is expected;
20
+ // accepting the destination does not authenticate this synthetic request.
21
+ if (connection.requestRejection({ headers: { host: url.host, origin: url.origin } }) === 403) {
22
+ return { state: 'untrusted-origin', origin: url.origin };
23
+ }
24
+ return { state: 'ready', origin: url.origin };
25
+ }
26
+ /** Called only behind the official authenticated Fetch registry. */
27
+ export function connectResponse(connection, configured) {
28
+ return async (request) => {
29
+ const info = connectInfo(connection, configured);
30
+ if (request.method === 'GET')
31
+ return Response.json(info, { headers });
32
+ // Explicit same-origin action; a navigation/form must never reveal a login URL.
33
+ // The official HTTP bridge uses an internal Request URL. Compare the
34
+ // preserved Host/Origin headers, as the official trust fence does, so an
35
+ // HTTPS reverse proxy does not turn a valid action into a false 403.
36
+ let sameOrigin = false;
37
+ try {
38
+ const origin = new URL(request.headers.get('origin') ?? '');
39
+ const host = request.headers.get('host');
40
+ sameOrigin = !!host && ['http:', 'https:'].includes(origin.protocol)
41
+ && origin.host === new URL(`http://${host}`).host;
42
+ }
43
+ catch { /* opaque or absent origin */ }
44
+ if (!sameOrigin || request.headers.get('content-type') !== 'application/json') {
45
+ return Response.json({ error: 'forbidden' }, { status: 403, headers });
46
+ }
47
+ if (info.state !== 'ready' || !info.origin)
48
+ return Response.json(info, { status: 409, headers });
49
+ try {
50
+ const login = new URL(connection.authenticatedUrl(info.origin));
51
+ if (login.origin !== info.origin || login.pathname !== '/' || login.username || login.password || login.hash
52
+ || !login.searchParams.get('token') || [...login.searchParams].length !== 1)
53
+ throw new Error('unsupported');
54
+ return Response.json({ origin: info.origin, loginUrl: login.href }, { headers });
55
+ }
56
+ catch {
57
+ // Never echo a provider error that might contain the launch credential.
58
+ return Response.json({ error: 'unavailable' }, { status: 503, headers });
59
+ }
60
+ };
61
+ }
@@ -0,0 +1,5 @@
1
+ import type { MobileDirectoryListing } from './protocol.ts';
2
+ /** Read-only companion for native-picker hosts; never changes the shared picker backend. */
3
+ export declare function listMobileDirectory(path: string | undefined, signal: AbortSignal): Promise<MobileDirectoryListing>;
4
+ export declare function directoryResponse(request: Request): Promise<Response>;
5
+ //# sourceMappingURL=directories.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { opendir, stat } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ /** Read-only companion for native-picker hosts; never changes the shared picker backend. */
5
+ export async function listMobileDirectory(path, signal) {
6
+ if (path !== undefined && (!path || path.length > 4096 || path.includes('\0') || !isAbsolute(path)
7
+ || (process.platform === 'win32' && !/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)/.test(path))))
8
+ throw new Error('invalid-path');
9
+ signal.throwIfAborted();
10
+ const target = resolve(path ?? homedir());
11
+ const entries = [];
12
+ let scanned = 0, truncated = false;
13
+ for await (const entry of await opendir(target)) {
14
+ signal.throwIfAborted();
15
+ if (++scanned > 10000 || entries.length >= 1000) {
16
+ truncated = true;
17
+ break;
18
+ }
19
+ const child = join(target, entry.name);
20
+ if (entry.isDirectory() || (entry.isSymbolicLink() && await stat(child).then(s => s.isDirectory(), () => false))) {
21
+ entries.push({ name: entry.name, path: child, hidden: entry.name.startsWith('.') });
22
+ }
23
+ }
24
+ signal.throwIfAborted();
25
+ entries.sort((a, b) => a.name.localeCompare(b.name));
26
+ return { path: target, parent: dirname(target) === target ? null : dirname(target), entries, truncated };
27
+ }
28
+ export async function directoryResponse(request) {
29
+ try {
30
+ const path = new URL(request.url).searchParams.get('path') ?? undefined;
31
+ return Response.json(await listMobileDirectory(path, request.signal), { headers: { 'cache-control': 'no-store' } });
32
+ }
33
+ catch (error) {
34
+ const invalid = error instanceof Error && error.message === 'invalid-path';
35
+ return Response.json({ error: invalid ? 'invalid-path' : 'directory-unavailable' }, {
36
+ status: request.signal.aborted ? 499 : invalid ? 400 : 422, headers: { 'cache-control': 'no-store' },
37
+ });
38
+ }
39
+ }
@@ -0,0 +1,9 @@
1
+ /** Mobile's host face uses the official authenticated Connection registry. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export declare const name = "mobile";
4
+ export declare const inject: string[];
5
+ /** Attach only when the official Connection carrier is composed. */
6
+ export declare function apply(ctx: Context, config?: {
7
+ publicOrigin?: string;
8
+ }): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { CONNECT_PATH, DIRECTORY_PATH, HANDSHAKE_PATH, mobileHandshake } from './protocol.js';
2
+ import { connectResponse } from './connect.js';
3
+ import { directoryResponse } from './directories.js';
4
+ export const name = 'mobile';
5
+ export const inject = [];
6
+ /** Attach only when the official Connection carrier is composed. */
7
+ export function apply(ctx, config = {}) {
8
+ ctx.inject(['connection'], (web) => {
9
+ web.connection.fetch.register({ path: CONNECT_PATH, methods: ['GET', 'POST'], requestBody: 'buffered',
10
+ fetch: connectResponse(web.connection, config.publicOrigin ?? process.env.DSH_MOBILE_PUBLIC_ORIGIN) });
11
+ web.connection.fetch.register({ path: DIRECTORY_PATH, methods: ['GET'], requestBody: 'buffered', fetch: directoryResponse });
12
+ web.connection.fetch.register({
13
+ path: HANDSHAKE_PATH,
14
+ methods: ['GET'],
15
+ requestBody: 'buffered',
16
+ fetch: async () => Response.json(mobileHandshake(), {
17
+ headers: { 'cache-control': 'no-store' },
18
+ }),
19
+ });
20
+ });
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-mobile`.
3
+ * @module @khorsheed/dsh-mobile/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "mobile-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-mobile`.
3
+ * @module @khorsheed/dsh-mobile/invariant
4
+ */
5
+ const PACKAGE_NAME = '@khorsheed/dsh-mobile';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'mobile-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /** Mobile owns no session data relations; the host owns authentication and streams. */
11
+ const install = () => { };
12
+ /**
13
+ * Register this package's invariant companion.
14
+ * @param ctx - Cordis context carrying the invariant service.
15
+ * @returns the installed registration's disposer after setup succeeds.
16
+ */
17
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
@@ -0,0 +1,30 @@
1
+ /** Identifiers shared by the plugin and its optional native shell. */
2
+ export declare const MOBILE_VERSION = "0.1.0";
3
+ export declare const BRIDGE_VERSION = 1;
4
+ export declare const HANDSHAKE_PATH = "/api/mobile/handshake";
5
+ export declare const CONNECT_PATH = "/api/mobile/connect";
6
+ export interface MobileConnectInfo {
7
+ state: 'ready' | 'not-configured' | 'invalid-origin' | 'untrusted-origin' | 'unsupported';
8
+ origin: string | null;
9
+ }
10
+ /** Read-only feature discovery; this does not create a login or grant access. */
11
+ export declare function mobileHandshake(): {
12
+ readonly mobileVersion: "0.1.0";
13
+ readonly bridgeVersion: 1;
14
+ readonly capabilities: readonly ['responsive-layout', 'official-composer', 'browser-session-auth'];
15
+ readonly authentication: 'official-browser-session';
16
+ readonly devicePairing: false;
17
+ readonly pushNotifications: false;
18
+ };
19
+ export declare const DIRECTORY_PATH = "/api/mobile/directories";
20
+ export interface MobileDirectoryListing {
21
+ path: string;
22
+ parent: string | null;
23
+ entries: {
24
+ name: string;
25
+ path: string;
26
+ hidden: boolean;
27
+ }[];
28
+ truncated: boolean;
29
+ }
30
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1,17 @@
1
+ /** Identifiers shared by the plugin and its optional native shell. */
2
+ export const MOBILE_VERSION = '0.1.0';
3
+ export const BRIDGE_VERSION = 1;
4
+ export const HANDSHAKE_PATH = '/api/mobile/handshake';
5
+ export const CONNECT_PATH = '/api/mobile/connect';
6
+ /** Read-only feature discovery; this does not create a login or grant access. */
7
+ export function mobileHandshake() {
8
+ return {
9
+ mobileVersion: MOBILE_VERSION,
10
+ bridgeVersion: BRIDGE_VERSION,
11
+ capabilities: ['responsive-layout', 'official-composer', 'browser-session-auth'],
12
+ authentication: 'official-browser-session',
13
+ devicePairing: false,
14
+ pushNotifications: false,
15
+ };
16
+ }
17
+ export const DIRECTORY_PATH = '/api/mobile/directories';
package/locale/en.json ADDED
@@ -0,0 +1 @@
1
+ {"meta": {"title": "Mobile", "description": "Mobile layout and conversation library with an iOS bridge"}}
package/locale/zh.json ADDED
@@ -0,0 +1 @@
1
+ {"meta": {"title": "移动端", "description": "提供移动布局、会话首页和 iOS 客户端桥接。"}}
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@khorsheed/dsh-mobile",
3
+ "version": "0.1.0",
4
+ "description": "Removable mobile presentation and iOS bridge for DeepSeek Harness.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "dsh-plugin",
9
+ "mobile",
10
+ "ios"
11
+ ],
12
+ "main": "lib/index.js",
13
+ "types": "lib/types/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./lib/types/index.d.ts",
17
+ "default": "./lib/index.js"
18
+ },
19
+ "./client": {
20
+ "types": "./lib/types/client/index.d.ts",
21
+ "default": "./lib/client.js"
22
+ },
23
+ "./invariant": {
24
+ "types": "./lib/types/invariant.d.ts",
25
+ "default": "./lib/invariant.js"
26
+ },
27
+ "./package.json": "./package.json",
28
+ "./locale/*.json": "./locale/*.json"
29
+ },
30
+ "files": [
31
+ "lib",
32
+ "cordis.patch.yml",
33
+ "locale/*.json",
34
+ "examples",
35
+ "README.md",
36
+ "README.zh.md",
37
+ "LICENSE"
38
+ ],
39
+ "dsh": {
40
+ "bundle": {
41
+ "patch": "./cordis.patch.yml"
42
+ },
43
+ "client": {
44
+ "platform": "web",
45
+ "inject": [
46
+ "@deepseek-ai/dsh-client-ui-layout",
47
+ "@deepseek-ai/dsh-client-locale",
48
+ "@deepseek-ai/dsh-client-connection"
49
+ ]
50
+ },
51
+ "compat": {
52
+ "minHost": "0.1.5-rc.1",
53
+ "notes": "Mobile layout and iOS shell under validation; Safari live-reply resume on Host 0.1.5-rc.1 requires the opt-in bundled ingress adapter. Device pairing and push are not provided."
54
+ }
55
+ },
56
+ "peerDependencies": {
57
+ "@deepseek-ai/cordis": "^4.0.1",
58
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
59
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
60
+ "@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.6",
61
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
62
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.0-rc.6",
63
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
64
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
65
+ "react": "^18.2.0",
66
+ "@deepseek-ai/dsh-client-ui-workspace": "^0.1.0-rc.6",
67
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.0-rc.6",
68
+ "@deepseek-ai/dsh-api-workspace-controller": "^0.1.0-rc.6"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@deepseek-ai/dsh-client-connection": {
72
+ "optional": true
73
+ },
74
+ "@deepseek-ai/dsh-client-locale": {
75
+ "optional": true
76
+ },
77
+ "@deepseek-ai/dsh-client-ui-layout": {
78
+ "optional": true
79
+ },
80
+ "@deepseek-ai/dsh-client-ui-slots": {
81
+ "optional": true
82
+ },
83
+ "@deepseek-ai/dsh-client-ui-renderer": {
84
+ "optional": true
85
+ },
86
+ "@deepseek-ai/dsh-client-ui-conversation": {
87
+ "optional": true
88
+ },
89
+ "@deepseek-ai/dsh-invariants": {
90
+ "optional": true
91
+ },
92
+ "@deepseek-ai/dsh-client-ui-workspace": {
93
+ "optional": true
94
+ },
95
+ "@deepseek-ai/dsh-api-session-controller": {
96
+ "optional": true
97
+ },
98
+ "@deepseek-ai/dsh-api-workspace-controller": {
99
+ "optional": true
100
+ }
101
+ }
102
+ }