ai-remote 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 (114) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +7 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/protocols/index.js +35 -0
  5. package/dist/protocols/index.js.map +7 -0
  6. package/dist/protocols/rdp/buffer.js +170 -0
  7. package/dist/protocols/rdp/buffer.js.map +7 -0
  8. package/dist/protocols/rdp/caps.js +260 -0
  9. package/dist/protocols/rdp/caps.js.map +7 -0
  10. package/dist/protocols/rdp/cert.js +135 -0
  11. package/dist/protocols/rdp/cert.js.map +7 -0
  12. package/dist/protocols/rdp/client.js +1068 -0
  13. package/dist/protocols/rdp/client.js.map +7 -0
  14. package/dist/protocols/rdp/cliprdr.js +240 -0
  15. package/dist/protocols/rdp/cliprdr.js.map +7 -0
  16. package/dist/protocols/rdp/credssp.js +254 -0
  17. package/dist/protocols/rdp/credssp.js.map +7 -0
  18. package/dist/protocols/rdp/crypto.js +369 -0
  19. package/dist/protocols/rdp/crypto.js.map +7 -0
  20. package/dist/protocols/rdp/display.js +589 -0
  21. package/dist/protocols/rdp/display.js.map +7 -0
  22. package/dist/protocols/rdp/gcc.js +132 -0
  23. package/dist/protocols/rdp/gcc.js.map +7 -0
  24. package/dist/protocols/rdp/keymap.js +132 -0
  25. package/dist/protocols/rdp/keymap.js.map +7 -0
  26. package/dist/protocols/rdp/mcs.js +223 -0
  27. package/dist/protocols/rdp/mcs.js.map +7 -0
  28. package/dist/protocols/rdp/ntlm.js +305 -0
  29. package/dist/protocols/rdp/ntlm.js.map +7 -0
  30. package/dist/protocols/rdp/pdu.js +444 -0
  31. package/dist/protocols/rdp/pdu.js.map +7 -0
  32. package/dist/protocols/rdp/rail.js +383 -0
  33. package/dist/protocols/rdp/rail.js.map +7 -0
  34. package/dist/protocols/rdp/rle.js +276 -0
  35. package/dist/protocols/rdp/rle.js.map +7 -0
  36. package/dist/protocols/rdp/sec.js +156 -0
  37. package/dist/protocols/rdp/sec.js.map +7 -0
  38. package/dist/protocols/rdp/session.js +307 -0
  39. package/dist/protocols/rdp/session.js.map +7 -0
  40. package/dist/protocols/rdp/tls.js +354 -0
  41. package/dist/protocols/rdp/tls.js.map +7 -0
  42. package/dist/protocols/rdp/vchannel.js +59 -0
  43. package/dist/protocols/rdp/vchannel.js.map +7 -0
  44. package/dist/protocols/rdp/x224.js +110 -0
  45. package/dist/protocols/rdp/x224.js.map +7 -0
  46. package/dist/protocols/ssh/kex.js +177 -0
  47. package/dist/protocols/ssh/kex.js.map +7 -0
  48. package/dist/protocols/ssh/messages.js +55 -0
  49. package/dist/protocols/ssh/messages.js.map +7 -0
  50. package/dist/protocols/ssh/session.js +608 -0
  51. package/dist/protocols/ssh/session.js.map +7 -0
  52. package/dist/protocols/ssh/terminal.js +191 -0
  53. package/dist/protocols/ssh/terminal.js.map +7 -0
  54. package/dist/protocols/ssh/transport.js +611 -0
  55. package/dist/protocols/ssh/transport.js.map +7 -0
  56. package/dist/protocols/ssh/wire.js +189 -0
  57. package/dist/protocols/ssh/wire.js.map +7 -0
  58. package/dist/protocols/types.js +1 -0
  59. package/dist/protocols/types.js.map +7 -0
  60. package/dist/protocols/vnc/input.js +16 -0
  61. package/dist/protocols/vnc/input.js.map +7 -0
  62. package/dist/protocols/vnc/session.js +259 -0
  63. package/dist/protocols/vnc/session.js.map +7 -0
  64. package/dist/shared/connection.js +114 -0
  65. package/dist/shared/connection.js.map +7 -0
  66. package/dist/shared/device.js +1 -0
  67. package/dist/shared/device.js.map +7 -0
  68. package/dist/shared/hosts.js +133 -0
  69. package/dist/shared/hosts.js.map +7 -0
  70. package/dist/shared/icons.js +42 -0
  71. package/dist/shared/icons.js.map +7 -0
  72. package/dist/shared/protocol.js +40 -0
  73. package/dist/shared/protocol.js.map +7 -0
  74. package/dist/shared/signals.js +42 -0
  75. package/dist/shared/signals.js.map +7 -0
  76. package/package.json +82 -0
  77. package/src/index.ts +14 -0
  78. package/src/protocols/index.ts +72 -0
  79. package/src/protocols/rdp/buffer.ts +204 -0
  80. package/src/protocols/rdp/caps.ts +304 -0
  81. package/src/protocols/rdp/cert.ts +190 -0
  82. package/src/protocols/rdp/client.ts +1363 -0
  83. package/src/protocols/rdp/cliprdr.ts +344 -0
  84. package/src/protocols/rdp/credssp.ts +319 -0
  85. package/src/protocols/rdp/crypto.ts +416 -0
  86. package/src/protocols/rdp/display.ts +750 -0
  87. package/src/protocols/rdp/gcc.ts +162 -0
  88. package/src/protocols/rdp/keymap.ts +68 -0
  89. package/src/protocols/rdp/mcs.ts +283 -0
  90. package/src/protocols/rdp/ntlm.ts +380 -0
  91. package/src/protocols/rdp/pdu.ts +497 -0
  92. package/src/protocols/rdp/rail.ts +480 -0
  93. package/src/protocols/rdp/rle.ts +343 -0
  94. package/src/protocols/rdp/sec.ts +184 -0
  95. package/src/protocols/rdp/session.ts +393 -0
  96. package/src/protocols/rdp/tls.ts +452 -0
  97. package/src/protocols/rdp/vchannel.ts +89 -0
  98. package/src/protocols/rdp/x224.ts +139 -0
  99. package/src/protocols/ssh/kex.ts +227 -0
  100. package/src/protocols/ssh/messages.ts +54 -0
  101. package/src/protocols/ssh/session.ts +878 -0
  102. package/src/protocols/ssh/terminal.ts +242 -0
  103. package/src/protocols/ssh/transport.ts +732 -0
  104. package/src/protocols/ssh/wire.ts +220 -0
  105. package/src/protocols/types.ts +146 -0
  106. package/src/protocols/vnc/input.ts +13 -0
  107. package/src/protocols/vnc/novnc.d.ts +75 -0
  108. package/src/protocols/vnc/session.ts +367 -0
  109. package/src/shared/connection.ts +206 -0
  110. package/src/shared/device.ts +28 -0
  111. package/src/shared/hosts.ts +240 -0
  112. package/src/shared/icons.ts +49 -0
  113. package/src/shared/protocol.ts +55 -0
  114. package/src/shared/signals.ts +67 -0
@@ -0,0 +1,220 @@
1
+ // SSH wire types (RFC 4251 section 5).
2
+ //
3
+ // Everything in SSH is big endian, so unlike the RDP reader and writer the byte
4
+ // order is not part of each call. The four types that matter here are the
5
+ // 32-bit length-prefixed string, the multiple-precision integer, the name-list
6
+ // and the byte.
7
+
8
+ const textEncoder = new TextEncoder();
9
+ const textDecoder = new TextDecoder();
10
+
11
+ export function encodeUtf8(text: string): Uint8Array {
12
+ return textEncoder.encode(text);
13
+ }
14
+
15
+ export function decodeUtf8(bytes: Uint8Array): string {
16
+ return textDecoder.decode(bytes);
17
+ }
18
+
19
+ export function concatBytes(...parts: readonly Uint8Array[]): Uint8Array {
20
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
21
+ const out = new Uint8Array(total);
22
+ let offset = 0;
23
+ for (const part of parts) {
24
+ out.set(part, offset);
25
+ offset += part.length;
26
+ }
27
+ return out;
28
+ }
29
+
30
+ /** Constant-time-ish equality. Used for authentication tags and fingerprints. */
31
+ export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
32
+ if (a.length !== b.length) return false;
33
+ let difference = 0;
34
+ for (let index = 0; index < a.length; index++) difference |= (a[index] ?? 0) ^ (b[index] ?? 0);
35
+ return difference === 0;
36
+ }
37
+
38
+ export function toBase64(bytes: Uint8Array): string {
39
+ let binary = '';
40
+ for (const byte of bytes) binary += String.fromCharCode(byte);
41
+ return btoa(binary);
42
+ }
43
+
44
+ export function fromBase64(text: string): Uint8Array {
45
+ const binary = atob(text);
46
+ const bytes = new Uint8Array(binary.length);
47
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
48
+ return bytes;
49
+ }
50
+
51
+ /** Base64url without padding, which is what a JSON Web Key wants. */
52
+ export function toBase64Url(bytes: Uint8Array): string {
53
+ return toBase64(bytes).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
54
+ }
55
+
56
+ export class SshWriter {
57
+ private bytes: Uint8Array<ArrayBuffer>;
58
+ private view: DataView;
59
+ private offset = 0;
60
+
61
+ constructor(capacity = 256) {
62
+ this.bytes = new Uint8Array(new ArrayBuffer(capacity));
63
+ this.view = new DataView(this.bytes.buffer);
64
+ }
65
+
66
+ #ensure(extra: number): void {
67
+ if (this.offset + extra <= this.bytes.length) return;
68
+ let capacity = Math.max(this.bytes.length * 2, 64);
69
+ while (capacity < this.offset + extra) capacity *= 2;
70
+ const grown = new Uint8Array(new ArrayBuffer(capacity));
71
+ grown.set(this.bytes.subarray(0, this.offset));
72
+ this.bytes = grown;
73
+ this.view = new DataView(grown.buffer);
74
+ }
75
+
76
+ u8(value: number): this {
77
+ this.#ensure(1);
78
+ this.view.setUint8(this.offset, value);
79
+ this.offset += 1;
80
+ return this;
81
+ }
82
+
83
+ boolean(value: boolean): this {
84
+ return this.u8(value ? 1 : 0);
85
+ }
86
+
87
+ u32(value: number): this {
88
+ this.#ensure(4);
89
+ this.view.setUint32(this.offset, value >>> 0, false);
90
+ this.offset += 4;
91
+ return this;
92
+ }
93
+
94
+ raw(bytes: Uint8Array): this {
95
+ this.#ensure(bytes.length);
96
+ this.bytes.set(bytes, this.offset);
97
+ this.offset += bytes.length;
98
+ return this;
99
+ }
100
+
101
+ /** A length-prefixed string. Text is encoded as UTF-8. */
102
+ string(value: string | Uint8Array): this {
103
+ const bytes = typeof value === 'string' ? encodeUtf8(value) : value;
104
+ return this.u32(bytes.length).raw(bytes);
105
+ }
106
+
107
+ nameList(names: readonly string[]): this {
108
+ return this.string(names.join(','));
109
+ }
110
+
111
+ /**
112
+ * An mpint: two's complement, big endian, with no leading zero bytes except
113
+ * the one that keeps a positive number from looking negative.
114
+ */
115
+ mpint(bytes: Uint8Array): this {
116
+ let start = 0;
117
+ while (start < bytes.length && bytes[start] === 0) start++;
118
+ const magnitude = bytes.subarray(start);
119
+ if (magnitude.length === 0) return this.u32(0);
120
+ if ((magnitude[0] ?? 0) & 0x80) {
121
+ this.u32(magnitude.length + 1).u8(0);
122
+ return this.raw(magnitude);
123
+ }
124
+ return this.u32(magnitude.length).raw(magnitude);
125
+ }
126
+
127
+ get length(): number {
128
+ return this.offset;
129
+ }
130
+
131
+ take(): Uint8Array {
132
+ return this.bytes.slice(0, this.offset);
133
+ }
134
+ }
135
+
136
+ export class SshReader {
137
+ private readonly bytes: Uint8Array;
138
+ private readonly view: DataView;
139
+ offset: number;
140
+
141
+ constructor(bytes: Uint8Array, offset = 0) {
142
+ this.bytes = bytes;
143
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
144
+ this.offset = offset;
145
+ }
146
+
147
+ get remaining(): number {
148
+ return this.bytes.length - this.offset;
149
+ }
150
+
151
+ #need(count: number): void {
152
+ if (count < 0 || this.remaining < count) {
153
+ throw new Error(`SSH: truncated packet (needed ${count} bytes, ${this.remaining} left)`);
154
+ }
155
+ }
156
+
157
+ u8(): number {
158
+ this.#need(1);
159
+ return this.view.getUint8(this.offset++);
160
+ }
161
+
162
+ boolean(): boolean {
163
+ return this.u8() !== 0;
164
+ }
165
+
166
+ u32(): number {
167
+ this.#need(4);
168
+ const value = this.view.getUint32(this.offset, false);
169
+ this.offset += 4;
170
+ return value;
171
+ }
172
+
173
+ raw(count: number): Uint8Array {
174
+ this.#need(count);
175
+ const slice = this.bytes.subarray(this.offset, this.offset + count);
176
+ this.offset += count;
177
+ return slice;
178
+ }
179
+
180
+ /** The bytes of a length-prefixed string. */
181
+ stringBytes(): Uint8Array {
182
+ return this.raw(this.u32());
183
+ }
184
+
185
+ string(): string {
186
+ return decodeUtf8(this.stringBytes());
187
+ }
188
+
189
+ nameList(): string[] {
190
+ const value = this.string();
191
+ return value ? value.split(',') : [];
192
+ }
193
+
194
+ /** An mpint's magnitude, with the sign byte removed. */
195
+ mpint(): Uint8Array {
196
+ const bytes = this.stringBytes();
197
+ let start = 0;
198
+ while (start < bytes.length && bytes[start] === 0) start++;
199
+ return bytes.subarray(start);
200
+ }
201
+
202
+ skip(count: number): this {
203
+ this.#need(count);
204
+ this.offset += count;
205
+ return this;
206
+ }
207
+
208
+ rest(): Uint8Array {
209
+ return this.bytes.subarray(this.offset);
210
+ }
211
+ }
212
+
213
+ /** Pad a magnitude out to a fixed width, which raw curve points need. */
214
+ export function padStart(bytes: Uint8Array, width: number): Uint8Array {
215
+ if (bytes.length === width) return bytes;
216
+ if (bytes.length > width) return bytes.subarray(bytes.length - width);
217
+ const out = new Uint8Array(width);
218
+ out.set(bytes, width - bytes.length);
219
+ return out;
220
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The interface every remote-desktop protocol presents to the UI.
3
+ *
4
+ * The workspace -- toolbar, stats rail, on-screen keyboard, reconnects -- does
5
+ * not care whether a session is VNC or RDP. Everything that does differ is
6
+ * reached through this interface instead of branching at each call site.
7
+ */
8
+
9
+ import type { ConnectionSettings, RemoteAppLaunch } from '../shared/connection';
10
+ import type { RdpSecurity, RemoteProtocol } from '../shared/protocol';
11
+
12
+ /** Where the pointer may go, in framebuffer pixels. */
13
+ export interface ScreenGeometry {
14
+ width: number;
15
+ height: number;
16
+ viewport: { x: number; y: number; width: number; height: number };
17
+ }
18
+
19
+ /** Mouse buttons, as the UI names them. */
20
+ export type PointerButton = 0 | 1 | 2;
21
+
22
+ /** Wheel movement in discrete remote-protocol notches. Positive is right/down. */
23
+ export interface ScrollDelta {
24
+ x: number;
25
+ y: number;
26
+ }
27
+
28
+ /**
29
+ * A live session. Both drivers expose this, so the AI pane's screen tool and
30
+ * the toolbar never have to ask which protocol they are driving.
31
+ */
32
+ export interface RemoteSession extends EventTarget {
33
+ readonly protocol: RemoteProtocol;
34
+ disconnect(): void;
35
+
36
+ /** True once the desktop has been reached. */
37
+ readonly connected: boolean;
38
+
39
+ /** Fit the remote desktop to the pane rather than scrolling it. */
40
+ scaleViewport: boolean;
41
+ /** Watch without sending keyboard or pointer input. */
42
+ viewOnly: boolean;
43
+ /** RFB quality level. RDP ignores it. */
44
+ qualityLevel?: number;
45
+ /** RFB compression level. RDP ignores it. */
46
+ compressionLevel?: number;
47
+
48
+ /** The transport the metrics rail instruments. Can appear a tick after construction. */
49
+ readonly socket?: WebSocket | null;
50
+
51
+ readonly screenGeometry: ScreenGeometry;
52
+
53
+ movePointer(x: number, y: number): boolean;
54
+ clickPointer(x: number, y: number, button?: PointerButton): boolean;
55
+ pointerButton(x: number, y: number, button?: PointerButton, pressed?: boolean): boolean;
56
+ scrollPointer(x: number, y: number, delta: ScrollDelta): boolean;
57
+ typeText(text: string): boolean;
58
+
59
+ /** Prefer the protocol clipboard for longer text when it can paste reliably. */
60
+ pasteText?(text: string): boolean | Promise<boolean>;
61
+
62
+ /** Press or release one key by keysym. */
63
+ sendKey(keysym: number, code: string | null, down?: boolean): void;
64
+
65
+ /** Send text to the host's clipboard, when the protocol carries one. */
66
+ sendClipboard?(text: string): void;
67
+
68
+ /**
69
+ * Stop taking keystrokes: the workspace has given the keyboard to something
70
+ * local, such as the terminal or the assistant's message box.
71
+ *
72
+ * Both protocols listen on the element they paint into, so a key only leaves
73
+ * for the host while that element holds focus. Handing focus back is the
74
+ * whole mechanism -- plus, for RDP, letting go of anything still held down,
75
+ * which would otherwise stay down on the far side.
76
+ */
77
+ releaseKeyboard?(): void;
78
+
79
+ /** Ask the host for a different desktop size, when the protocol allows it. */
80
+ requestDesktopSize?(width: number, height: number): void;
81
+
82
+ /** The canvas the session paints into, for screenshots and thumbnails. */
83
+ readonly canvas?: HTMLCanvasElement | null;
84
+ }
85
+
86
+ /** What the workspace hands a driver to open a session. */
87
+ export interface SessionOptions {
88
+ credentials: { username: string; password: string };
89
+ domain: string;
90
+ security: RdpSecurity;
91
+ remoteApp: RemoteAppLaunch | null;
92
+
93
+ /** Desktop size to ask the host for. */
94
+ width: number;
95
+ height: number;
96
+ /** False on a phone, where resizing the host would make a portrait strip of it. */
97
+ allowRemoteResize: boolean;
98
+
99
+ scaleViewport: boolean;
100
+ viewOnly: boolean;
101
+ quality: number;
102
+ compression: number;
103
+
104
+ /** Shown in this driver's own log lines. */
105
+ hostLabel: string;
106
+ }
107
+
108
+ /** Wording for the one connect dialog, which is shared markup across protocols. */
109
+ export interface DialogCopy {
110
+ usernameLabel: string;
111
+ usernamePlaceholder: string;
112
+ usernameHelp: string;
113
+ passwordLabel: string;
114
+ passwordPlaceholder: string;
115
+ passwordHelp: string;
116
+ /** A VNC session needs some secret; RDP can sign in on the host's own screen. */
117
+ requiresPassword: boolean;
118
+ }
119
+
120
+ /** Everything the UI knows about a close, before anyone tries to explain it. */
121
+ export interface DisconnectContext {
122
+ code?: number;
123
+ reason: string;
124
+ /** Pre-rendered ` (WebSocket 1006: ...)` tail, so wording stays consistent. */
125
+ suffix: string;
126
+ }
127
+
128
+ /** One protocol implementation. */
129
+ export interface ProtocolDriver {
130
+ readonly protocol: RemoteProtocol;
131
+ /** Used as the prefix of this driver's console lines. */
132
+ readonly label: string;
133
+ readonly defaultPort: number;
134
+ readonly dialog: DialogCopy;
135
+
136
+ /** The gateway endpoint for this connection, including any query it needs. */
137
+ buildUrl(origin: string, settings: ConnectionSettings): string;
138
+
139
+ createSession(container: HTMLElement, url: string, options: SessionOptions): RemoteSession;
140
+
141
+ /** Extra detail worth logging once the first server frame has been parsed. */
142
+ describeState(session: RemoteSession): Record<string, unknown>;
143
+
144
+ /** Protocol-specific wording for a close, or null to fall back to the shared text. */
145
+ describeDisconnect(context: DisconnectContext): string | null;
146
+ }
@@ -0,0 +1,13 @@
1
+ /** RFB wheel buttons for a bounded two-axis scroll, in wire order. */
2
+ export function vncWheelMasks(delta: { x: number; y: number }): number[] {
3
+ const masks: number[] = [];
4
+ const wheels = [
5
+ { amount: delta.y, negativeMask: 1 << 3, positiveMask: 1 << 4 },
6
+ { amount: delta.x, negativeMask: 1 << 5, positiveMask: 1 << 6 },
7
+ ];
8
+ for (const { amount, negativeMask, positiveMask } of wheels) {
9
+ const mask = amount < 0 ? negativeMask : positiveMask;
10
+ for (let step = 0; step < Math.min(20, Math.abs(amount)); step++) masks.push(mask);
11
+ }
12
+ return masks;
13
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * noVNC ships no type declarations, so this is the surface this project uses.
3
+ *
4
+ * It deliberately includes the underscore-prefixed internals the VNC driver
5
+ * reaches for. Those are private by convention, and naming them here is the
6
+ * honest record of that: the pointer helpers need framebuffer-space coordinates,
7
+ * which noVNC's own event handlers compute and then keep to themselves.
8
+ */
9
+
10
+ declare module '@novnc/novnc' {
11
+ export interface RfbCredentials {
12
+ username?: string;
13
+ password?: string;
14
+ target?: string;
15
+ }
16
+
17
+ export interface RfbOptions {
18
+ credentials?: RfbCredentials;
19
+ shared?: boolean;
20
+ repeaterID?: string;
21
+ wsProtocols?: string[];
22
+ }
23
+
24
+ /** noVNC's own socket wrapper. `_websocket` is the transport underneath. */
25
+ export interface RfbSocket {
26
+ _websocket: WebSocket | null;
27
+ _rQlen: number;
28
+ _rQi: number;
29
+ }
30
+
31
+ export default class RFB extends EventTarget {
32
+ constructor(target: HTMLElement, url: string, options?: RfbOptions);
33
+
34
+ static messages: {
35
+ fbUpdateRequest(
36
+ sock: RfbSocket,
37
+ incremental: boolean,
38
+ x: number,
39
+ y: number,
40
+ width: number,
41
+ height: number
42
+ ): void;
43
+ pointerEvent(sock: RfbSocket, x: number, y: number, mask: number): void;
44
+ };
45
+
46
+ disconnect(): void;
47
+ sendCredentials(credentials: RfbCredentials): void;
48
+ sendKey(keysym: number, code: string | null, down?: boolean): void;
49
+ sendCtrlAltDel(): void;
50
+ clipboardPasteFrom(text: string): void;
51
+ focus(): void;
52
+ blur(): void;
53
+ machineShutdown(): void;
54
+
55
+ viewOnly: boolean;
56
+ scaleViewport: boolean;
57
+ resizeSession: boolean;
58
+ clipViewport: boolean;
59
+ showDotCursor: boolean;
60
+ qualityLevel: number;
61
+ compressionLevel: number;
62
+ background: string;
63
+
64
+ readonly capabilities: { power: boolean };
65
+
66
+ /** 'connecting' | 'connected' | 'disconnecting' | 'disconnected'. */
67
+ _rfbConnectionState: string;
68
+ _rfbInitState: string;
69
+ _fbWidth: number;
70
+ _fbHeight: number;
71
+ _sock: RfbSocket | null;
72
+ _canvas: HTMLCanvasElement | null;
73
+ _cursor: { _canvas: HTMLCanvasElement | null } | null;
74
+ }
75
+ }