@or3/intern-client 0.1.1

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.
@@ -0,0 +1,114 @@
1
+ const REDACTED = '[REDACTED]';
2
+
3
+ function secretKey(key: string): boolean {
4
+ return (
5
+ /^(?:authorization|proxy-authorization|cookie|set-cookie|password|passphrase|secret|api[_-]?key)$/i.test(
6
+ key
7
+ ) ||
8
+ /(?:^|[_-])(?:password|passphrase|secret|token|api[_-]?key)$/i.test(
9
+ key
10
+ ) ||
11
+ /(?:Password|Passphrase|Secret|Token|ApiKey)$/.test(key)
12
+ );
13
+ }
14
+
15
+ function replaceLiteral(value: string, secret: string): string {
16
+ if (!secret) return value;
17
+ return value.split(secret).join(REDACTED);
18
+ }
19
+
20
+ function redactString(
21
+ input: string,
22
+ explicitSecrets: readonly string[]
23
+ ): string {
24
+ let value = input
25
+ .replace(/\bBearer\s+[^\s"',;]+/gi, `Bearer ${REDACTED}`)
26
+ .replace(
27
+ /(["']?(?:authorization|api[_-]?key|[a-z0-9_-]*(?:password|passphrase|secret|token))["']?\s*[:=]\s*["']?)([^"',}\s&]+)/gi,
28
+ `$1${REDACTED}`
29
+ );
30
+
31
+ value = value.replace(
32
+ /([?&])([^=&#]+)=([^&#]*)/g,
33
+ (match, prefix: string, rawKey: string) => {
34
+ let key = rawKey;
35
+ try {
36
+ key = decodeURIComponent(rawKey);
37
+ } catch {
38
+ // Keep the raw key when malformed URL encoding is encountered.
39
+ }
40
+ return secretKey(key)
41
+ ? `${prefix}${rawKey}=${REDACTED}`
42
+ : match;
43
+ }
44
+ );
45
+
46
+ for (const secret of explicitSecrets) {
47
+ value = replaceLiteral(value, secret);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function redactValue(
53
+ value: unknown,
54
+ explicitSecrets: readonly string[],
55
+ seen: WeakSet<object>
56
+ ): unknown {
57
+ if (typeof value === 'string') {
58
+ return redactString(value, explicitSecrets);
59
+ }
60
+ if (
61
+ value === null ||
62
+ value === undefined ||
63
+ typeof value === 'number' ||
64
+ typeof value === 'boolean' ||
65
+ typeof value === 'bigint'
66
+ ) {
67
+ return value;
68
+ }
69
+ if (value instanceof Error) {
70
+ return {
71
+ name: value.name,
72
+ message: redactString(value.message, explicitSecrets),
73
+ };
74
+ }
75
+ if (Array.isArray(value)) {
76
+ if (seen.has(value)) return '[Circular]';
77
+ seen.add(value);
78
+ return value.map((item) => redactValue(item, explicitSecrets, seen));
79
+ }
80
+ if (typeof value === 'object') {
81
+ if (seen.has(value)) return '[Circular]';
82
+ seen.add(value);
83
+ const output: Record<string, unknown> = {};
84
+ for (const [key, item] of Object.entries(value)) {
85
+ output[key] = secretKey(key)
86
+ ? REDACTED
87
+ : redactValue(item, explicitSecrets, seen);
88
+ }
89
+ return output;
90
+ }
91
+ return redactString(String(value), explicitSecrets);
92
+ }
93
+
94
+ export function redactInternSecrets(
95
+ value: unknown,
96
+ explicitSecrets: readonly string[] = []
97
+ ): unknown {
98
+ return redactValue(
99
+ value,
100
+ explicitSecrets.filter(Boolean),
101
+ new WeakSet()
102
+ );
103
+ }
104
+
105
+ export function safeInternStringify(
106
+ value: unknown,
107
+ explicitSecrets: readonly string[] = []
108
+ ): string {
109
+ try {
110
+ return JSON.stringify(redactInternSecrets(value, explicitSecrets));
111
+ } catch {
112
+ return '"[Unserializable]"';
113
+ }
114
+ }
package/src/sse.ts ADDED
@@ -0,0 +1,151 @@
1
+ export interface InternSseEvent<T = unknown> {
2
+ event?: string;
3
+ id?: string;
4
+ retry?: number;
5
+ data: string;
6
+ json?: T;
7
+ cursor?: string;
8
+ }
9
+
10
+ function parseJson<T>(data: string): T | undefined {
11
+ if (!data) return undefined;
12
+ try {
13
+ return JSON.parse(data) as T;
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ }
18
+
19
+ export function parseInternSseBlock<T = unknown>(
20
+ block: string
21
+ ): InternSseEvent<T> {
22
+ const data: string[] = [];
23
+ const output: InternSseEvent<T> = { data: '' };
24
+
25
+ for (const line of block.split(/\r\n|\r|\n/)) {
26
+ if (!line || line.startsWith(':')) continue;
27
+ const separator = line.indexOf(':');
28
+ const field = separator < 0 ? line : line.slice(0, separator);
29
+ const rawValue = separator < 0 ? '' : line.slice(separator + 1);
30
+ const value = rawValue.startsWith(' ')
31
+ ? rawValue.slice(1)
32
+ : rawValue;
33
+ switch (field) {
34
+ case 'event':
35
+ output.event = value;
36
+ break;
37
+ case 'id':
38
+ if (!value.includes('\0')) output.id = value;
39
+ break;
40
+ case 'retry': {
41
+ const retry = Number(value);
42
+ if (Number.isInteger(retry) && retry >= 0) {
43
+ output.retry = retry;
44
+ }
45
+ break;
46
+ }
47
+ case 'data':
48
+ data.push(value);
49
+ break;
50
+ }
51
+ }
52
+
53
+ output.data = data.join('\n');
54
+ output.json = parseJson<T>(output.data);
55
+ if (output.id) output.cursor = output.id;
56
+ return output;
57
+ }
58
+
59
+ function splitSseBuffer(buffer: string): {
60
+ blocks: string[];
61
+ remainder: string;
62
+ } {
63
+ const blocks: string[] = [];
64
+ let start = 0;
65
+ const delimiter = /\r\n\r\n|\n\n|\r\r/g;
66
+ for (let match = delimiter.exec(buffer); match; match = delimiter.exec(buffer)) {
67
+ blocks.push(buffer.slice(start, match.index));
68
+ start = match.index + match[0].length;
69
+ }
70
+ return { blocks, remainder: buffer.slice(start) };
71
+ }
72
+
73
+ export async function* readInternSseStream<T = unknown>(
74
+ stream: ReadableStream<Uint8Array>,
75
+ signal?: AbortSignal
76
+ ): AsyncIterable<InternSseEvent<T>> {
77
+ const reader = stream.getReader();
78
+ const decoder = new TextDecoder();
79
+ let buffer = '';
80
+
81
+ const abort = () => {
82
+ void reader.cancel().catch(() => undefined);
83
+ };
84
+ signal?.addEventListener('abort', abort, { once: true });
85
+
86
+ try {
87
+ while (true) {
88
+ if (signal?.aborted) {
89
+ throw signal.reason ?? new DOMException('Aborted', 'AbortError');
90
+ }
91
+ const { done, value } = await reader.read();
92
+ if (done) break;
93
+ buffer += decoder.decode(value, { stream: true });
94
+ const { blocks, remainder } = splitSseBuffer(buffer);
95
+ buffer = remainder;
96
+ for (const block of blocks) {
97
+ if (block.trim() && !block.trimStart().startsWith(':')) {
98
+ yield parseInternSseBlock<T>(block);
99
+ }
100
+ }
101
+ }
102
+ buffer += decoder.decode();
103
+ if (buffer.trim() && !buffer.trimStart().startsWith(':')) {
104
+ yield parseInternSseBlock<T>(buffer);
105
+ }
106
+ } finally {
107
+ signal?.removeEventListener('abort', abort);
108
+ reader.releaseLock();
109
+ }
110
+ }
111
+
112
+ export function internSseEventKey(
113
+ event: InternSseEvent<unknown>
114
+ ): string | undefined {
115
+ if (event.id) return `sse:${event.id}`;
116
+ const payload =
117
+ event.json && typeof event.json === 'object' && !Array.isArray(event.json)
118
+ ? (event.json as Record<string, unknown>)
119
+ : undefined;
120
+ if (!payload) return undefined;
121
+ if (
122
+ (typeof payload.id === 'string' || typeof payload.id === 'number') &&
123
+ String(payload.id) !== ''
124
+ ) {
125
+ return `payload:${String(payload.id)}`;
126
+ }
127
+ if (
128
+ typeof payload.turn_id === 'string' &&
129
+ (typeof payload.seq === 'string' || typeof payload.seq === 'number')
130
+ ) {
131
+ return `turn:${payload.turn_id}:${String(payload.seq)}`;
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ export function internSseEventCursor(
137
+ event: InternSseEvent<unknown>
138
+ ): string | undefined {
139
+ if (event.id) return event.id;
140
+ const payload =
141
+ event.json && typeof event.json === 'object' && !Array.isArray(event.json)
142
+ ? (event.json as Record<string, unknown>)
143
+ : undefined;
144
+ if (
145
+ payload &&
146
+ (typeof payload.seq === 'string' || typeof payload.seq === 'number')
147
+ ) {
148
+ return String(payload.seq);
149
+ }
150
+ return undefined;
151
+ }