@tienne/gestalt 0.108.0 → 0.109.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 (69) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/package.json +1 -1
  3. package/dist/src/cli/commands/peer.d.ts +19 -0
  4. package/dist/src/cli/commands/peer.d.ts.map +1 -0
  5. package/dist/src/cli/commands/peer.js +147 -0
  6. package/dist/src/cli/commands/peer.js.map +1 -0
  7. package/dist/src/cli/index.d.ts.map +1 -1
  8. package/dist/src/cli/index.js +35 -2
  9. package/dist/src/cli/index.js.map +1 -1
  10. package/dist/src/peer/client.d.ts +9 -0
  11. package/dist/src/peer/client.d.ts.map +1 -0
  12. package/dist/src/peer/client.js +51 -0
  13. package/dist/src/peer/client.js.map +1 -0
  14. package/dist/src/peer/daemon.d.ts +71 -0
  15. package/dist/src/peer/daemon.d.ts.map +1 -0
  16. package/dist/src/peer/daemon.js +407 -0
  17. package/dist/src/peer/daemon.js.map +1 -0
  18. package/dist/src/peer/errors.d.ts +2 -0
  19. package/dist/src/peer/errors.d.ts.map +1 -0
  20. package/dist/src/peer/errors.js +4 -0
  21. package/dist/src/peer/errors.js.map +1 -0
  22. package/dist/src/peer/index.d.ts +14 -0
  23. package/dist/src/peer/index.d.ts.map +1 -0
  24. package/dist/src/peer/index.js +9 -0
  25. package/dist/src/peer/index.js.map +1 -0
  26. package/dist/src/peer/locate.d.ts +44 -0
  27. package/dist/src/peer/locate.d.ts.map +1 -0
  28. package/dist/src/peer/locate.js +116 -0
  29. package/dist/src/peer/locate.js.map +1 -0
  30. package/dist/src/peer/paths.d.ts +23 -0
  31. package/dist/src/peer/paths.d.ts.map +1 -0
  32. package/dist/src/peer/paths.js +50 -0
  33. package/dist/src/peer/paths.js.map +1 -0
  34. package/dist/src/peer/protocol.d.ts +61 -0
  35. package/dist/src/peer/protocol.d.ts.map +1 -0
  36. package/dist/src/peer/protocol.js +7 -0
  37. package/dist/src/peer/protocol.js.map +1 -0
  38. package/dist/src/peer/redact.d.ts +30 -0
  39. package/dist/src/peer/redact.d.ts.map +1 -0
  40. package/dist/src/peer/redact.js +214 -0
  41. package/dist/src/peer/redact.js.map +1 -0
  42. package/dist/src/peer/share.d.ts +123 -0
  43. package/dist/src/peer/share.d.ts.map +1 -0
  44. package/dist/src/peer/share.js +204 -0
  45. package/dist/src/peer/share.js.map +1 -0
  46. package/dist/src/peer/state.d.ts +37 -0
  47. package/dist/src/peer/state.d.ts.map +1 -0
  48. package/dist/src/peer/state.js +99 -0
  49. package/dist/src/peer/state.js.map +1 -0
  50. package/dist/src/peer/tail.d.ts +55 -0
  51. package/dist/src/peer/tail.d.ts.map +1 -0
  52. package/dist/src/peer/tail.js +148 -0
  53. package/dist/src/peer/tail.js.map +1 -0
  54. package/dist/src/peer/transform.d.ts +79 -0
  55. package/dist/src/peer/transform.d.ts.map +1 -0
  56. package/dist/src/peer/transform.js +237 -0
  57. package/dist/src/peer/transform.js.map +1 -0
  58. package/dist/src/peer/viewer-html.d.ts +10 -0
  59. package/dist/src/peer/viewer-html.d.ts.map +1 -0
  60. package/dist/src/peer/viewer-html.js +157 -0
  61. package/dist/src/peer/viewer-html.js.map +1 -0
  62. package/dist/src/peer/viewer.d.ts +58 -0
  63. package/dist/src/peer/viewer.d.ts.map +1 -0
  64. package/dist/src/peer/viewer.js +249 -0
  65. package/dist/src/peer/viewer.js.map +1 -0
  66. package/package.json +1 -1
  67. package/plugin/.codex-plugin/plugin.json +1 -1
  68. package/plugin/.mcp.json +1 -1
  69. package/plugin/mcp.json +1 -1
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 데몬이 소켓과 상태 DB를 두는 자리. 기본은 `~/.gestalt/peer`이고 테스트는
3
+ * `GESTALT_PEER_HOME`으로 옮긴다.
4
+ *
5
+ * 상대경로는 받지 않고 던진다. `GESTALT_HOME`처럼 진짜 홈으로 돌아가면 테스트가
6
+ * 실수로 사용자의 데몬 소켓에 붙거나 사용자 DB에 쓴다.
7
+ */
8
+ export declare function peerRoot(env?: NodeJS.ProcessEnv): string;
9
+ export declare function socketPath(root: string): string;
10
+ export declare function statePath(root: string): string;
11
+ /**
12
+ * 같은 OS 사용자만 들어오게 디렉토리를 0700으로 만든다. 이미 있으면 권한을 다시
13
+ * 좁힌다 — 소켓은 listen과 chmod 사이에 잠깐 기본 권한으로 열려 있어서 그 틈을
14
+ * 막는 건 디렉토리다.
15
+ *
16
+ * 루트가 심링크거나 남의 것이면 거부한다. mkdir과 chmod는 심링크를 따라가서 남이 걸어
17
+ * 둔 링크 끝의 디렉토리에 소켓과 DB를 만들고 권한까지 바꾼다. realpath로 비교하지 않는
18
+ * 건 macOS 임시 디렉토리가 `/var` → `/private/var` 링크 아래라 정상 경로도 걸리기 때문이다.
19
+ */
20
+ export declare function ensurePrivateDir(dir: string): void;
21
+ /** macOS 유닉스 소켓 경로 한도(sun_path 104바이트) */
22
+ export declare const MAX_SOCKET_PATH_BYTES = 103;
23
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../../src/peer/paths.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAKrE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAUlD;AAED,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,MAAM,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { chmodSync, lstatSync, mkdirSync } from 'node:fs';
2
+ import { isAbsolute, join, normalize } from 'node:path';
3
+ import { gestaltHome } from '../core/home.js';
4
+ const PEER_HOME_ENV = 'GESTALT_PEER_HOME';
5
+ /**
6
+ * 데몬이 소켓과 상태 DB를 두는 자리. 기본은 `~/.gestalt/peer`이고 테스트는
7
+ * `GESTALT_PEER_HOME`으로 옮긴다.
8
+ *
9
+ * 상대경로는 받지 않고 던진다. `GESTALT_HOME`처럼 진짜 홈으로 돌아가면 테스트가
10
+ * 실수로 사용자의 데몬 소켓에 붙거나 사용자 DB에 쓴다.
11
+ */
12
+ export function peerRoot(env = process.env) {
13
+ const raw = env[PEER_HOME_ENV];
14
+ if (raw === undefined || raw === '')
15
+ return join(gestaltHome(), 'peer');
16
+ if (!isAbsolute(raw))
17
+ throw new Error(`${PEER_HOME_ENV}는 절대경로여야 해요: ${raw}`);
18
+ return normalize(raw);
19
+ }
20
+ export function socketPath(root) {
21
+ return join(root, 'peer.sock');
22
+ }
23
+ export function statePath(root) {
24
+ return join(root, 'state.db');
25
+ }
26
+ /**
27
+ * 같은 OS 사용자만 들어오게 디렉토리를 0700으로 만든다. 이미 있으면 권한을 다시
28
+ * 좁힌다 — 소켓은 listen과 chmod 사이에 잠깐 기본 권한으로 열려 있어서 그 틈을
29
+ * 막는 건 디렉토리다.
30
+ *
31
+ * 루트가 심링크거나 남의 것이면 거부한다. mkdir과 chmod는 심링크를 따라가서 남이 걸어
32
+ * 둔 링크 끝의 디렉토리에 소켓과 DB를 만들고 권한까지 바꾼다. realpath로 비교하지 않는
33
+ * 건 macOS 임시 디렉토리가 `/var` → `/private/var` 링크 아래라 정상 경로도 걸리기 때문이다.
34
+ */
35
+ export function ensurePrivateDir(dir) {
36
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
37
+ const st = lstatSync(dir);
38
+ if (st.isSymbolicLink())
39
+ throw new Error(`peer 디렉토리가 심링크예요: ${dir}`);
40
+ if (!st.isDirectory())
41
+ throw new Error(`peer 디렉토리 자리에 다른 파일이 있어요: ${dir}`);
42
+ const uid = process.getuid?.();
43
+ if (uid !== undefined && st.uid !== uid) {
44
+ throw new Error(`peer 디렉토리의 주인이 내가 아니에요: ${dir}`);
45
+ }
46
+ chmodSync(dir, 0o700);
47
+ }
48
+ /** macOS 유닉스 소켓 경로 한도(sun_path 104바이트) */
49
+ export const MAX_SOCKET_PATH_BYTES = 103;
50
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../../src/peer/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC;IAC/B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC;IACxE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,aAAa,gBAAgB,GAAG,EAAE,CAAC,CAAC;IAC7E,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACjD,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,EAAE,CAAC,cAAc,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,EAAE,CAAC,CAAC;IACrE,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAC;IAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IAC/B,IAAI,GAAG,KAAK,SAAS,IAAI,EAAE,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;AACxB,CAAC;AAED,0CAA0C;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC"}
@@ -0,0 +1,61 @@
1
+ import type { ToolsLevel } from './redact.js';
2
+ import type { ShareStatus } from './share.js';
3
+ /** IPC 한 줄 요청. 줄 단위 JSON이다 */
4
+ export type PeerRequest = {
5
+ op: 'ping';
6
+ } | {
7
+ op: 'status';
8
+ } | {
9
+ op: 'shares';
10
+ } | {
11
+ op: 'share';
12
+ transcriptPath: string;
13
+ sessionId?: string;
14
+ tools?: ToolsLevel;
15
+ expiresMinutes?: number;
16
+ } | {
17
+ op: 'unshare';
18
+ id: string;
19
+ };
20
+ export type PeerResponse<T = unknown> = {
21
+ ok: true;
22
+ result: T;
23
+ } | {
24
+ ok: false;
25
+ error: string;
26
+ };
27
+ export interface ShareInfo {
28
+ id: string;
29
+ sessionId: string | null;
30
+ transcriptPath: string;
31
+ tools: ToolsLevel;
32
+ createdAt: number;
33
+ expiresAt: number;
34
+ status: ShareStatus;
35
+ viewers: number;
36
+ events: number;
37
+ unknownLines: number;
38
+ /** 바꾸다 예외가 나서 버린 줄 */
39
+ failedLines: number;
40
+ /** 한 줄 상한을 넘어 버린 줄 */
41
+ oversizedLines: number;
42
+ /** 메모리 상한에 밀려 새 뷰어에게 다시 못 보내는 앞부분 이벤트 */
43
+ droppedEvents: number;
44
+ title?: string;
45
+ url: string;
46
+ }
47
+ export interface DaemonStatus {
48
+ pid: number;
49
+ root: string;
50
+ viewerPort: number;
51
+ startedAt: number;
52
+ shares: number;
53
+ viewers: number;
54
+ }
55
+ /** 한 요청 줄의 한도. 소켓은 0600이지만 같은 사용자의 프로세스가 메모리를 못 불리게 둔다 */
56
+ export declare const MAX_REQUEST_BYTES: number;
57
+ /** 기본 만료. 설계 원문의 8시간 */
58
+ export declare const DEFAULT_EXPIRES_MINUTES: number;
59
+ /** 만료 상한. 공유를 사실상 영구로 열어 두지 못하게 한다 */
60
+ export declare const MAX_EXPIRES_MINUTES: number;
61
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/peer/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,8BAA8B;AAC9B,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GACd;IAAE,EAAE,EAAE,QAAQ,CAAA;CAAE,GAChB;IAAE,EAAE,EAAE,QAAQ,CAAA;CAAE,GAChB;IACE,EAAE,EAAE,OAAO,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GACD;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,0DAA0D;AAC1D,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,wBAAwB;AACxB,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,sCAAsC;AACtC,eAAO,MAAM,mBAAmB,QAAc,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** 한 요청 줄의 한도. 소켓은 0600이지만 같은 사용자의 프로세스가 메모리를 못 불리게 둔다 */
2
+ export const MAX_REQUEST_BYTES = 64 * 1024;
3
+ /** 기본 만료. 설계 원문의 8시간 */
4
+ export const DEFAULT_EXPIRES_MINUTES = 8 * 60;
5
+ /** 만료 상한. 공유를 사실상 영구로 열어 두지 못하게 한다 */
6
+ export const MAX_EXPIRES_MINUTES = 7 * 24 * 60;
7
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/peer/protocol.ts"],"names":[],"mappings":"AAiDA,0DAA0D;AAC1D,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C,wBAAwB;AACxB,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,EAAE,CAAC;AAE9C,sCAAsC;AACtC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC"}
@@ -0,0 +1,30 @@
1
+ export declare const REDACTED = "[\uAC00\uB9BC]";
2
+ /** `--tools summary`일 때 도구 결과를 자르는 길이 */
3
+ export declare const TOOL_RESULT_SUMMARY_BYTES: number;
4
+ export type ToolsLevel = 'full' | 'summary' | 'none';
5
+ export declare const TOOLS_LEVELS: readonly ToolsLevel[];
6
+ export declare function isToolsLevel(value: unknown): value is ToolsLevel;
7
+ export interface RedactOptions {
8
+ /** `~`로 바꿀 홈 디렉토리. 테스트가 가짜 홈을 넣는다 */
9
+ home?: string;
10
+ }
11
+ export declare function redactText(text: string, options?: RedactOptions): string;
12
+ /** 키를 `_`, `-`, `.`, camelCase 경계로 나눠 민감한 낱말이 있는지 본다 */
13
+ export declare function isSensitiveKey(key: string): boolean;
14
+ /**
15
+ * 도구 입력처럼 모양을 모르는 값을 가린다. 문자열은 패턴으로 가리고 민감한 키 아래
16
+ * 값은 패턴과 상관없이 가린다. `{ password: "hunter2" }`의 값은 어떤 패턴에도 안 걸린다.
17
+ *
18
+ * 민감한 키 아래가 평범한 객체면 안으로 들어가고 원시값이나 배열이면 통째로 가린다.
19
+ * 배열까지 들어가면 `cookies: ["sid=abc"]`의 원소가 그대로 나간다.
20
+ */
21
+ export declare function redactValue(value: unknown, options?: RedactOptions): unknown;
22
+ /**
23
+ * UTF-8 바이트 기준으로 앞부분만 남긴다. 반드시 가린 **뒤에** 자른다 — 먼저 자르면
24
+ * PEM 끝 표시가 잘려 나가거나 토큰 앞부분만 남아 패턴에 안 걸린다.
25
+ */
26
+ export declare function truncateBytes(text: string, maxBytes: number): {
27
+ text: string;
28
+ truncated: boolean;
29
+ };
30
+ //# sourceMappingURL=redact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../../src/peer/redact.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,QAAQ,mBAAS,CAAC;AAE/B,yCAAyC;AACzC,eAAO,MAAM,yBAAyB,QAAW,CAAC;AAElD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAErD,eAAO,MAAM,YAAY,EAAE,SAAS,UAAU,EAAgC,CAAC;AAE/E,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAEhE;AAED,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAmID,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAkB5E;AAyCD,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAYhF;AAMD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAOtC"}
@@ -0,0 +1,214 @@
1
+ import { homedir } from 'node:os';
2
+ export const REDACTED = '[가림]';
3
+ /** `--tools summary`일 때 도구 결과를 자르는 길이 */
4
+ export const TOOL_RESULT_SUMMARY_BYTES = 4 * 1024;
5
+ export const TOOLS_LEVELS = ['full', 'summary', 'none'];
6
+ export function isToolsLevel(value) {
7
+ return typeof value === 'string' && TOOLS_LEVELS.includes(value);
8
+ }
9
+ /**
10
+ * 비밀값 패턴. 순서가 의미 있다 — PEM 블록을 먼저 통째로 지워야 그 안의 줄이
11
+ * 다른 패턴에 반쯤 걸려 조각이 남지 않는다.
12
+ *
13
+ * 다 잡지는 못한다. 그래서 공유를 열 때와 뷰어 상단에 같은 경고를 띄운다.
14
+ */
15
+ const PATTERNS = [
16
+ // 끝 표시가 없으면(잘린 출력) 끝까지 지운다. 본문 일부가 남는 것보다 낫다
17
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g,
18
+ /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/g,
19
+ /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g,
20
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
21
+ /\bglpat-[A-Za-z0-9_-]{20,}/g,
22
+ /\bnpm_[A-Za-z0-9]{30,}/g,
23
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
24
+ /\bsk-ant-[A-Za-z0-9_-]{16,}/g,
25
+ /\bsk-[A-Za-z0-9_-]{20,}/g,
26
+ /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/g,
27
+ /\bAIza[A-Za-z0-9_-]{35}/g,
28
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/g,
29
+ ];
30
+ /**
31
+ * `https://user:pass@host`. 누구로 붙는지는 남기고 비밀번호만 지운다. Redis 접속 문자열에
32
+ * 흔한 `redis://:pass@host`처럼 이름 없이 비밀번호만 둔 꼴도 잡으려고 이름 자리를 비워도 받는다.
33
+ */
34
+ const URL_PASSWORD = /\b([a-z][a-z0-9+.-]{0,20}:\/\/[^\s:/@]{0,256}:)[^\s/@]{1,256}@/gi;
35
+ /** `Authorization: Bearer xxx` 같은 헤더. 이름은 남기고 값만 지운다 */
36
+ const BEARER = /\b(Bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi;
37
+ /**
38
+ * `Basic` 뒤 base64 값. 대소문자를 가리고 값에 숫자나 `+`, `/`, `=`가 있거나 둘째 글자부터
39
+ * 대문자가 있어야 건다. `Basic authentication`이나 `Basic Configuration` 같은 영어 문장은
40
+ * 그대로 둔다. 헤더 꼴이 아닌 `{ authorization: 'Basic …' }` 같은 객체 출력은 이 패턴만 잡는다.
41
+ * 전부 대문자인 `BASIC`은 헤더 줄과 따옴표 안 헤더 패턴에만 맡긴다. 여기서 받으면 `BASIC SETTINGS` 같은 대문자
42
+ * 제목이 가려진다.
43
+ *
44
+ * 값은 base64 글자로만 되고 끝의 `=` 채움까지 쳐서 길이가 4의 배수인 8자 이상이어야 한다.
45
+ * 그래야 `Basic README.md`나 `Basic 2024-10-28` 같은 낱말이 값으로 안 읽힌다. 값 뒤에
46
+ * 마침표가 와도 그 뒤에 글자가 오면 값으로 안 본다. 문장 끝 마침표 앞의 값은 가리고
47
+ * `Ubuntu22.04` 같은 낱말은 남긴다.
48
+ */
49
+ const BASIC = /\b(Basic)[ \t]+(?=[A-Za-z0-9+/]*?[0-9+/=]|[A-Za-z0-9+/][a-z0-9+/]*?[A-Z])(?:(?:[A-Za-z0-9+/]{4})+(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)|(?:[A-Za-z0-9+/]{4}){2,})(?![A-Za-z0-9+/=_~-]|\.[A-Za-z0-9])/g;
50
+ /**
51
+ * 헤더 이름. token과 secret은 앞에 하이픈으로 이어진 조각이 있을 때만 받는다. 그래야
52
+ * `X-Auth-Token`과 `PRIVATE-TOKEN`은 걸리고 코드의 `token: string;`이나 `max_tokens: 100`은
53
+ * 줄 끝까지 가려지지 않는다.
54
+ */
55
+ const HEADER_NAME = '(?:set-cookie|cookie|(?:[a-z0-9]+-)*api-key|(?:[a-z0-9]+-)+(?:token|secret)|(?:proxy-)?authorization)';
56
+ /**
57
+ * `curl -v`가 찍는 `> Cookie: …`나 `< X-Auth-Token: …` 줄. 값에 정해진 꼴이 없어서 콜론 뒤를
58
+ * 줄 끝까지 가린다. `Bearer`나 `Basic`은 머리에 넣어 BEARER와 BASIC이 남긴 `Bearer [가림]`과
59
+ * `Basic [가림]`을 그대로 둔다.
60
+ */
61
+ const HEADER_LINE = new RegExp(String.raw `^([ \t]*(?:[<>][ \t]*)?${HEADER_NAME}[ \t]*:[ \t]*(?:(?:Bearer|Basic)[ \t]+)?)[^\n]+`, 'gim');
62
+ /**
63
+ * `curl -H "X-Api-Key: …" https://…`처럼 따옴표 안에 든 헤더. 줄 끝까지 가리면 뒤 URL과
64
+ * 옵션까지 사라지므로 닫는 따옴표 앞에서 멈춘다.
65
+ */
66
+ const HEADER_QUOTED = new RegExp(String.raw `(["'])([ \t]*${HEADER_NAME}[ \t]*:[ \t]*(?:(?:Bearer|Basic)[ \t]+)?)[^"'\n]+`, 'gi');
67
+ /**
68
+ * `API_KEY=…`, `export GH_TOKEN=…`처럼 이름에 KEY, TOKEN, SECRET, PASSWORD, PASSWD, PASS,
69
+ * PWD, COOKIE, CREDENTIAL(S), AUTH가 든 대입. 이름은 남겨야 무엇이 가려졌는지 읽힌다. `=` 앞
70
+ * 공백은 안 받는다. 코드의 `key = value`까지 걸면 도구 출력이 거의 다 가려진다.
71
+ *
72
+ * PASS와 AUTH는 앞뒤가 글자가 아닐 때만 받는다. 안 그러면 `BYPASS=1`, `--bypass=true`,
73
+ * `OAUTH_CLIENT_ID=…`, `AUTHOR=…`가 걸린다. PWD는 `_PWD`만 받는다. 셸이 찍는 `PWD=`와
74
+ * `OLDPWD=`는 비밀이 아니라 작업 경로다.
75
+ *
76
+ * 매치는 이름 속 KEY 같은 낱말에서 시작한다. 그 앞의 이름 조각은 매치 밖이라 그대로
77
+ * 남는다. 이름 앞부분까지 정규식에 넣으면 `a.a.a.…`나 `KEYKEY…` 같은 입력에서 시작점마다
78
+ * 앞뒤 조합을 다 훑느라 역추적이 터진다. 낱말 뒤 조각은 64자까지 미리보기로 한 번에
79
+ * 잡고 되돌아가지 않는다(`(?=(…))\2`).
80
+ *
81
+ * 값은 닫힌 따옴표, 따옴표 안 줄바꿈(닫는 따옴표를 4096자 안에서 찾는다), JSON 문자열
82
+ * 안에 이스케이프된 따옴표, 못 닫힌 따옴표(줄 끝까지), 맨 값 순서로 본다.
83
+ *
84
+ * 이스케이프된 따옴표 값은 백슬래시 개수로 갈라 읽는다. 한 번 더 감싸인 글자에서 값 안의
85
+ * 따옴표는 백슬래시 셋과 따옴표가 되고 값 끝 백슬래시는 닫는 따옴표 앞에 백슬래시 넷을
86
+ * 더 붙인다. 이 둘을 닫는 따옴표로 잘못 읽으면 뒷부분이 그대로 나가거나 다음 따옴표까지
87
+ * 넘어가 이웃 값이 새므로 넷, 셋과 따옴표, 둘, 하나를 서로 겹치지 않게 나눈다.
88
+ */
89
+ const ASSIGNMENT = /((?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|(?<![a-z])PASS(?![a-z])|_PWD(?![a-z])|COOKIE|CREDENTIALS?|(?<![a-z])AUTH(?![a-z]))(?=([A-Za-z0-9_.-]{0,64}))\2)=(?:"(?:[^"\\]|\\[\s\S]){0,4096}"|'[^']{0,4096}'|\\"(?:[^"\\\n]|\\[^"\\\n]|\\\\\\\\|\\\\\\"|\\\\[^"\\\n]){0,4096}\\"|\\?["'][^\n]*|[^\s"'\\]+)/gi;
90
+ /**
91
+ * JSON이나 YAML 꼴 `"api_key": "…"`. 큰따옴표 친 키만 본다. 대화 기록 안에서 JSON이
92
+ * 문자열로 한 번 더 감싸이면 `\"api_key\": \"…\"`가 되므로 이스케이프된 따옴표도 받는다.
93
+ * 감싼 값은 ASSIGNMENT와 같이 백슬래시 개수로 갈라 읽는다.
94
+ *
95
+ * 값 안에 `\n`으로 이스케이프하지 않은 줄바꿈이 나오면 닫는 따옴표를 찾지 않고 그 줄 끝까지만
96
+ * 가린다. JSON 문자열에는 이스케이프하지 않은 줄바꿈이 못 들어가므로 이건 따옴표가 안 닫힌
97
+ * 값이다. 다음 줄까지 넘어가 닫는 따옴표를 찾으면 그 줄에 있던 다른 민감한 키를 건너뛰어 그
98
+ * 값이 샌다.
99
+ *
100
+ * 낱말 목록은 isSensitiveKey와 따로 간다. 여기는 키 이름의 부분 문자열로 맞추므로
101
+ * `auth`는 뒤가, `pass`는 앞뒤가 글자가 아닐 때만 받는다. 안 그러면 `"author"`와
102
+ * `"passed"`, `"bypass"`가 걸린다.
103
+ */
104
+ const SENSITIVE_FIELD_WORD = '(?:key|token|secret|password|passwd|passphrase|(?<![a-z])pass(?![a-z])|pwd|cookie|credential|authorization|auth(?![a-z]))';
105
+ const QUOTED_FIELD = new RegExp(String.raw `(\\?"[A-Za-z0-9_.-]{0,64}${SENSITIVE_FIELD_WORD}[A-Za-z0-9_.-]{0,64}\\?"\s*:\s*)(?:"(?:[^"\\\n]|\\.){0,4096}"|\\"(?:[^"\\\n]|\\[^"\\\n]|\\\\\\\\|\\\\\\"|\\\\[^"\\\n]){0,4096}\\"|\\?"[^\n]*)`, 'gi');
106
+ /**
107
+ * Python이 찍는 `{'Cookie': 'sid=…'}`처럼 작은따옴표 친 키. QUOTED_FIELD와 같은 낱말을 쓴다.
108
+ * 값은 같은 줄에서 4096자 안에 닫는 작은따옴표가 있어야 가린다.
109
+ */
110
+ const SINGLE_QUOTED_FIELD = new RegExp(String.raw `('[A-Za-z0-9_.-]{0,64}${SENSITIVE_FIELD_WORD}[A-Za-z0-9_.-]{0,64}'\s*:\s*)'(?:[^'\\\n]|\\.){0,4096}'`, 'gi');
111
+ export function redactText(text, options = {}) {
112
+ let out = text;
113
+ for (const pattern of PATTERNS)
114
+ out = out.replace(pattern, REDACTED);
115
+ out = out.replace(URL_PASSWORD, (_m, head) => `${head}${REDACTED}@`);
116
+ out = out.replace(BEARER, (_m, scheme) => `${scheme} ${REDACTED}`);
117
+ out = out.replace(BASIC, (_m, scheme) => `${scheme} ${REDACTED}`);
118
+ out = out.replace(HEADER_LINE, (_m, head) => `${head}${REDACTED}`);
119
+ out = out.replace(HEADER_QUOTED, (_m, quote, head) => `${quote}${head}${REDACTED}`);
120
+ out = out.replace(ASSIGNMENT, (_m, name) => `${name}=${REDACTED}`);
121
+ out = out.replace(QUOTED_FIELD, (_m, head) => {
122
+ const q = head.startsWith('\\') ? '\\"' : '"';
123
+ return `${head}${q}${REDACTED}${q}`;
124
+ });
125
+ out = out.replace(SINGLE_QUOTED_FIELD, (_m, head) => `${head}'${REDACTED}'`);
126
+ return replaceHome(out, options.home ?? homedir());
127
+ }
128
+ function replaceHome(text, home) {
129
+ const trimmed = home.replace(/\/+$/, '');
130
+ // `/`나 빈 홈이면 모든 경로가 `~`로 바뀌어 읽을 수 없게 된다
131
+ if (trimmed.length < 2)
132
+ return text;
133
+ const escaped = trimmed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
134
+ // `/Users/alice`가 `/Users/alice2`까지 먹지 않게 뒤를 경로 경계로 묶는다
135
+ return text.replace(new RegExp(`${escaped}(?=[/\\s"'\`:;,)\\]}]|$)`, 'g'), '~');
136
+ }
137
+ /**
138
+ * 이름만 봐도 값이 비밀인 키. 낱말 단위로 맞춘다. `keyword`나 `author`처럼 낱말 일부만
139
+ * 겹치는 키는 안 건드린다.
140
+ *
141
+ * 소문자로 붙여 쓴 `accesstoken`이나 `privatekey` 같은 낱말은 SENSITIVE_KEY_SUFFIX로 낱말
142
+ * 끝을 본다. `key`와 `pass`는 낱말 끝만 보면 `monkey`나 `bypass`가 걸린다. 그래서 `pass`는
143
+ * 낱말이 똑같을 때만 걸고 `key`는 access, secret, private, signing, encryption, master, api가
144
+ * 바로 앞에 붙은 합성어만 받는다. token은 복수형을 안 받는다. `max_tokens`나
145
+ * `input_tokens`가 흔한 도구 입력 키다.
146
+ */
147
+ const SENSITIVE_KEY_WORDS = new Set([
148
+ 'key',
149
+ 'apikey',
150
+ 'token',
151
+ 'secret',
152
+ 'password',
153
+ 'passwd',
154
+ 'pass',
155
+ 'pwd',
156
+ 'auth',
157
+ 'authorization',
158
+ 'credential',
159
+ 'credentials',
160
+ 'cookie',
161
+ 'cookies',
162
+ ]);
163
+ const SENSITIVE_KEY_SUFFIX = /(?:token|(?:secret|password|passwd|passphrase)s?|(?:access|secret|private|signing|encryption|master|api)keys?)$/;
164
+ /** 키를 `_`, `-`, `.`, camelCase 경계로 나눠 민감한 낱말이 있는지 본다 */
165
+ export function isSensitiveKey(key) {
166
+ const words = key
167
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
168
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
169
+ .split(/[\s_.-]+/);
170
+ return words.some((w) => {
171
+ const lower = w.toLowerCase();
172
+ return SENSITIVE_KEY_WORDS.has(lower) || SENSITIVE_KEY_SUFFIX.test(lower);
173
+ });
174
+ }
175
+ /**
176
+ * 도구 입력처럼 모양을 모르는 값을 가린다. 문자열은 패턴으로 가리고 민감한 키 아래
177
+ * 값은 패턴과 상관없이 가린다. `{ password: "hunter2" }`의 값은 어떤 패턴에도 안 걸린다.
178
+ *
179
+ * 민감한 키 아래가 평범한 객체면 안으로 들어가고 원시값이나 배열이면 통째로 가린다.
180
+ * 배열까지 들어가면 `cookies: ["sid=abc"]`의 원소가 그대로 나간다.
181
+ */
182
+ export function redactValue(value, options = {}) {
183
+ if (typeof value === 'string')
184
+ return redactText(value, options);
185
+ if (Array.isArray(value))
186
+ return value.map((v) => redactValue(v, options));
187
+ if (value && typeof value === 'object') {
188
+ const out = {};
189
+ for (const [k, v] of Object.entries(value)) {
190
+ const hide = isSensitiveKey(k) && !isPlainObject(v);
191
+ out[redactText(k, options)] = hide ? REDACTED : redactValue(v, options);
192
+ }
193
+ return out;
194
+ }
195
+ return value;
196
+ }
197
+ function isPlainObject(value) {
198
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
199
+ }
200
+ /**
201
+ * UTF-8 바이트 기준으로 앞부분만 남긴다. 반드시 가린 **뒤에** 자른다 — 먼저 자르면
202
+ * PEM 끝 표시가 잘려 나가거나 토큰 앞부분만 남아 패턴에 안 걸린다.
203
+ */
204
+ export function truncateBytes(text, maxBytes) {
205
+ const buf = Buffer.from(text, 'utf8');
206
+ if (buf.length <= maxBytes)
207
+ return { text, truncated: false };
208
+ // 멀티바이트 글자 중간에서 끊기면 깨진 글자가 남는다. 이어지는 바이트(10xxxxxx)를 건너뛴다
209
+ let end = maxBytes;
210
+ while (end > 0 && ((buf[end] ?? 0) & 0xc0) === 0x80)
211
+ end--;
212
+ return { text: buf.subarray(0, end).toString('utf8'), truncated: true };
213
+ }
214
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../../src/peer/redact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ import type { TailOptions } from './tail.js';
2
+ import type { RedactOptions, ToolsLevel } from './redact.js';
3
+ /**
4
+ * 뷰어 하나가 받는 것. seq는 SSE id로 써서 다시 붙었을 때 이어 받게 한다. 이벤트는
5
+ * 쌓을 때 한 번 직렬화한 글자를 그대로 넘긴다. 뷰어마다 다시 직렬화하지 않는다.
6
+ */
7
+ export type ShareFrame = {
8
+ type: 'event';
9
+ seq: number;
10
+ json: string;
11
+ }
12
+ /** 붙은 뷰어가 받을 앞부분 중 메모리 상한에 밀려 버려진 이벤트 수 */
13
+ | {
14
+ type: 'trimmed';
15
+ dropped: number;
16
+ } | {
17
+ type: 'stats';
18
+ unknownLines: number;
19
+ } | {
20
+ type: 'state';
21
+ status: ShareStatus;
22
+ }
23
+ /**
24
+ * 공유 종료. 앞부분을 따라잡는 중에 종료되면 아직 못 보낸 이벤트는 사라지므로 그 개수를
25
+ * missed에 싣는다. 붙어 있던 뷰어는 다 받은 뒤라 안 싣는다
26
+ */
27
+ | {
28
+ type: 'closed';
29
+ reason: string;
30
+ missed?: number;
31
+ };
32
+ export type ShareStatus = 'live' | 'missing' | 'truncated';
33
+ export type ShareListener = (frame: ShareFrame) => void;
34
+ export interface ShareStreamOptions extends RedactOptions {
35
+ id: string;
36
+ transcriptPath: string;
37
+ tools: ToolsLevel;
38
+ /** 들고 있는 이벤트의 직렬화 바이트 상한. 테스트가 작게 준다 */
39
+ maxBacklogBytes?: number;
40
+ tail?: Omit<TailOptions, 'startOffset'>;
41
+ }
42
+ /**
43
+ * 새 뷰어에게 다시 보낼 앞부분의 상한. 넘으면 오래된 것부터 버린다. 공유는 최대 7일
44
+ * 열려 있고 도구 출력이 크면 events가 끝없이 는다.
45
+ */
46
+ export declare const DEFAULT_MAX_BACKLOG_BYTES: number;
47
+ export interface BacklogSlice {
48
+ /** `afterSeq` 뒤로 상한에 밀려 버려진 이벤트 수 */
49
+ dropped: number;
50
+ items: {
51
+ seq: number;
52
+ json: string;
53
+ }[];
54
+ }
55
+ /**
56
+ * 공유 하나. 대화 기록을 따라 읽어 바꾼 이벤트를 메모리에 쌓고 붙은 뷰어에 흘린다.
57
+ *
58
+ * 바꾼 결과만 메모리에 들고 원문 줄은 들지 않는다. 디스크에는 아무것도 안 쓴다.
59
+ * 처음 붙는 뷰어가 앞부분을 받아야 해서 바꾼 이벤트를 직렬화한 글자로 상한까지 남긴다.
60
+ *
61
+ * seq는 공유가 연 뒤로 나온 이벤트의 번호다. 앞부분을 버려도 번호는 안 당긴다.
62
+ * 배열 위치로 매기면 버린 만큼 번호가 밀려 Last-Event-ID로 이어 받을 때 겹치거나 빠진다.
63
+ */
64
+ export declare class ShareStream {
65
+ readonly id: string;
66
+ readonly transcriptPath: string;
67
+ readonly tools: ToolsLevel;
68
+ private tail;
69
+ private transformer;
70
+ /**
71
+ * `events[head]`부터가 살아 있다. 앞에서 하나씩 shift하면 큰 배열에서 매번 다 옮긴다.
72
+ * head 앞 칸은 버릴 때 비워 둔다. 압축은 head가 1024를 넘어야 돌아서 칸에 참조를
73
+ * 남기면 버린 이벤트를 GC가 못 가져간다.
74
+ */
75
+ private events;
76
+ private head;
77
+ /** `events[0]` 앞에서 잘라 낸 이벤트 수. `events[i]`의 seq는 `base + i + 1`이다 */
78
+ private base;
79
+ private backlogBytes;
80
+ private readonly maxBacklogBytes;
81
+ private listeners;
82
+ private status;
83
+ private title;
84
+ private lastUnknown;
85
+ private closedReason;
86
+ constructor(options: ShareStreamOptions);
87
+ get currentStatus(): ShareStatus;
88
+ get unknownLines(): number;
89
+ get failedLines(): number;
90
+ get oversizedLines(): number;
91
+ get claudeVersion(): string | undefined;
92
+ get sessionTitle(): string | undefined;
93
+ get eventCount(): number;
94
+ /** 상한에 밀려 버린 이벤트 수 */
95
+ get droppedEvents(): number;
96
+ get viewerCount(): number;
97
+ get isClosed(): boolean;
98
+ /**
99
+ * 새로 쓰인 줄을 읽어 흘린다. 데몬이 주기적으로 부른다.
100
+ * 모르는 줄 수가 늘었으면 true — 데몬이 감사 로그에 버전을 남길 때 쓴다.
101
+ */
102
+ poll(): {
103
+ unknownGrew: boolean;
104
+ };
105
+ /**
106
+ * 쌓인 것 중 `afterSeq` 뒤를 `maxBytes`쯤까지 꺼낸다. 이벤트 하나가 상한보다 커도 하나는
107
+ * 꺼낸다. 뷰어가 소켓이 비는 만큼씩 앞부분을 보내려고 쓴다. 한 번에 다 넘기면 안 읽는
108
+ * 뷰어마다 앞부분 전체가 소켓 버퍼에 쌓인다.
109
+ */
110
+ backlogAfter(afterSeq: number, maxBytes: number): BacklogSlice;
111
+ /**
112
+ * 뷰어를 붙인다. 지금 상태와 `afterSeq` 뒤 앞부분을 한 번에 보내고 이후는 생기는 대로.
113
+ * 떼어내는 함수를 돌려준다. 앞부분이 큰 뷰어는 backlogAfter로 먼저 따라잡은 뒤 이벤트
114
+ * 루프로 돌아가기 전에 바로 붙어야 사이에 poll이 끼어 이벤트가 빠지지 않는다.
115
+ */
116
+ subscribe(listener: ShareListener, afterSeq?: number): () => void;
117
+ close(reason: string): void;
118
+ /** 가장 최근 이벤트 하나는 상한을 넘어도 남긴다. 다 버리면 새 뷰어가 지금 상태를 못 본다 */
119
+ private trim;
120
+ private setStatus;
121
+ private emit;
122
+ }
123
+ //# sourceMappingURL=share.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../../../src/peer/share.ts"],"names":[],"mappings":"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"}