pi-cursor-sdk 0.1.55 → 0.1.57

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 (88) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/README.md +100 -28
  3. package/docs/cursor-live-smoke-checklist.md +7 -7
  4. package/docs/cursor-model-ux-spec.md +48 -41
  5. package/docs/cursor-native-tool-replay.md +4 -4
  6. package/docs/cursor-native-tool-visual-audit.md +1 -1
  7. package/docs/cursor-testing-lessons.md +20 -7
  8. package/docs/cursor-tool-surfaces.md +13 -2
  9. package/docs/platform-smoke-implementation.md +220 -0
  10. package/docs/platform-smoke.md +183 -247
  11. package/package.json +39 -7
  12. package/platform-smoke.config.mjs +5 -1
  13. package/scripts/cloud-runtime-smoke.d.mts +3 -0
  14. package/scripts/cloud-runtime-smoke.mjs +502 -0
  15. package/scripts/debug-provider-events.mjs +7 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -6
  17. package/scripts/lib/cursor-child-process.d.mts +1 -0
  18. package/scripts/lib/cursor-child-process.mjs +137 -7
  19. package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
  20. package/scripts/local-resume-cleanup-smoke.mjs +108 -0
  21. package/scripts/local-resume-smoke.d.mts +1 -0
  22. package/scripts/local-resume-smoke.mjs +642 -0
  23. package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
  24. package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
  25. package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
  26. package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
  27. package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
  28. package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
  29. package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
  30. package/scripts/platform-smoke/artifacts.mjs +293 -65
  31. package/scripts/platform-smoke/card-detect.mjs +16 -4
  32. package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
  33. package/scripts/platform-smoke/doctor.mjs +20 -10
  34. package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
  35. package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
  36. package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
  37. package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
  38. package/scripts/platform-smoke/scenarios.mjs +16 -2
  39. package/scripts/platform-smoke/target-runtime.mjs +206 -0
  40. package/scripts/platform-smoke/targets.mjs +33 -141
  41. package/scripts/platform-smoke/visual-evidence.mjs +6 -7
  42. package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
  43. package/scripts/platform-smoke.mjs +40 -27
  44. package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
  45. package/scripts/steering-rpc-smoke.mjs +12 -2
  46. package/scripts/tmux-live-smoke.sh +3 -5
  47. package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
  48. package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
  49. package/shared/cursor-sensitive-text.mjs +7 -1
  50. package/src/context.ts +5 -2
  51. package/src/cursor-agents-context-registration.ts +7 -0
  52. package/src/cursor-agents-context.ts +3 -1
  53. package/src/cursor-api-key.ts +15 -1
  54. package/src/cursor-bridge-contract.ts +3 -0
  55. package/src/cursor-cloud-lifecycle.ts +733 -0
  56. package/src/cursor-cloud-options.ts +206 -0
  57. package/src/cursor-cloud-reporting.ts +246 -0
  58. package/src/cursor-config.ts +659 -0
  59. package/src/cursor-display-only-trace.ts +14 -0
  60. package/src/cursor-display-text.ts +8 -2
  61. package/src/cursor-durable-fs.ts +49 -0
  62. package/src/cursor-fallback-models.generated.ts +2045 -485
  63. package/src/cursor-live-run-accounting.ts +7 -1
  64. package/src/cursor-live-run-coordinator.ts +1 -0
  65. package/src/cursor-pi-tool-bridge-run.ts +14 -4
  66. package/src/cursor-provider-errors.ts +31 -5
  67. package/src/cursor-provider-live-run-drain.ts +5 -0
  68. package/src/cursor-provider-run-finalizer.ts +37 -28
  69. package/src/cursor-provider-run-outcome.ts +7 -2
  70. package/src/cursor-provider-turn-finalize.ts +77 -7
  71. package/src/cursor-provider-turn-prepare.ts +228 -12
  72. package/src/cursor-provider-turn-runner.ts +42 -13
  73. package/src/cursor-provider-turn-send.ts +59 -16
  74. package/src/cursor-provider-turn-types.ts +44 -10
  75. package/src/cursor-runtime-state.ts +478 -0
  76. package/src/cursor-sdk-event-debug.ts +46 -6
  77. package/src/cursor-sdk-process-error-guard.ts +101 -30
  78. package/src/cursor-session-agent-cleanup.ts +328 -0
  79. package/src/cursor-session-agent-resume.ts +439 -0
  80. package/src/cursor-session-agent.ts +109 -13
  81. package/src/cursor-session-scope.ts +35 -2
  82. package/src/cursor-session-send-policy.ts +1 -1
  83. package/src/cursor-skill-tool.ts +30 -11
  84. package/src/cursor-state.ts +112 -69
  85. package/src/cursor-tool-manifest.ts +1 -1
  86. package/src/cursor-usage-accounting.ts +14 -4
  87. package/src/index.ts +11 -2
  88. package/src/model-discovery.ts +10 -56
@@ -0,0 +1,335 @@
1
+ #define _DARWIN_C_SOURCE
2
+ #define _POSIX_C_SOURCE 200809L
3
+
4
+ #include <errno.h>
5
+ #include <fcntl.h>
6
+ #include <stdint.h>
7
+ #include <stdio.h>
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+ #include <sys/stat.h>
11
+ #include <unistd.h>
12
+
13
+ #ifndef MAX_INPUT
14
+ #define MAX_INPUT (64u * 1024u * 1024u)
15
+ #endif
16
+ #ifndef MAX_FILES
17
+ #define MAX_FILES 512u
18
+ #endif
19
+ #ifndef MAX_FILE_BYTES
20
+ #define MAX_FILE_BYTES (5u * 1024u * 1024u)
21
+ #endif
22
+ #ifndef MAX_TOTAL_BYTES
23
+ #define MAX_TOTAL_BYTES (40u * 1024u * 1024u)
24
+ #endif
25
+ #ifndef MAX_PATH_BYTES
26
+ #define MAX_PATH_BYTES 4096u
27
+ #endif
28
+ #ifndef MAX_PATH_COMPONENTS
29
+ #define MAX_PATH_COMPONENTS 4096u
30
+ #endif
31
+ #define MAX_CREATED_ENTRIES (MAX_PATH_COMPONENTS + MAX_FILES)
32
+
33
+ typedef struct {
34
+ char *path;
35
+ const unsigned char *content;
36
+ uint32_t size;
37
+ } InputFile;
38
+
39
+ typedef struct {
40
+ char *path;
41
+ char *name;
42
+ dev_t dev;
43
+ ino_t ino;
44
+ int parent_fd;
45
+ int directory;
46
+ } CreatedEntry;
47
+
48
+ static uint32_t read_u32(const unsigned char *bytes) {
49
+ return (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24);
50
+ }
51
+
52
+ static int canonical_path(const char *path, size_t length) {
53
+ if (length == 0 || length > MAX_PATH_BYTES || path[0] == '/' ||
54
+ (length >= 3 && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) &&
55
+ path[1] == ':' && path[2] == '/')) return 0;
56
+ size_t start = 0;
57
+ for (size_t index = 0; index <= length; index++) {
58
+ if (index < length && path[index] != '/') {
59
+ if (path[index] == '\\' || path[index] == '\0') return 0;
60
+ continue;
61
+ }
62
+ const size_t segment = index - start;
63
+ if (segment == 0 || (segment == 1 && path[start] == '.') ||
64
+ (segment == 2 && path[start] == '.' && path[start + 1] == '.')) return 0;
65
+ start = index + 1;
66
+ }
67
+ return 1;
68
+ }
69
+
70
+ static int conflicting_paths(const char *left, const char *right) {
71
+ const size_t left_length = strlen(left);
72
+ const size_t right_length = strlen(right);
73
+ if (strcmp(left, right) == 0) return 1;
74
+ return (left_length < right_length && strncmp(left, right, left_length) == 0 && right[left_length] == '/') ||
75
+ (right_length < left_length && strncmp(right, left, right_length) == 0 && left[right_length] == '/');
76
+ }
77
+
78
+ static int read_input(unsigned char **buffer, size_t *length) {
79
+ size_t capacity = 64u * 1024u;
80
+ unsigned char *bytes = malloc(capacity);
81
+ if (!bytes) return 0;
82
+ *length = 0;
83
+ for (;;) {
84
+ if (*length == capacity) {
85
+ if (capacity >= MAX_INPUT) { free(bytes); return 0; }
86
+ capacity *= 2;
87
+ if (capacity > MAX_INPUT) capacity = MAX_INPUT;
88
+ unsigned char *grown = realloc(bytes, capacity);
89
+ if (!grown) { free(bytes); return 0; }
90
+ bytes = grown;
91
+ }
92
+ const ssize_t count = read(STDIN_FILENO, bytes + *length, capacity - *length);
93
+ if (count < 0) { if (errno == EINTR) continue; free(bytes); return 0; }
94
+ if (count == 0) break;
95
+ *length += (size_t)count;
96
+ }
97
+ *buffer = bytes;
98
+ return 1;
99
+ }
100
+
101
+ static int parse_input(unsigned char *buffer, size_t length, InputFile **files_out, uint32_t *count_out) {
102
+ static const unsigned char magic[8] = { 'P', 'I', 'A', 'R', 'T', '0', '1', 0 };
103
+ if (length < 12 || memcmp(buffer, magic, sizeof(magic)) != 0) return 0;
104
+ const uint32_t count = read_u32(buffer + 8);
105
+ if (count > MAX_FILES) return 0;
106
+ if (count == 0) {
107
+ if (length != 12) return 0;
108
+ *files_out = NULL;
109
+ *count_out = 0;
110
+ return 1;
111
+ }
112
+ InputFile *files = calloc(count, sizeof(*files));
113
+ if (!files) return 0;
114
+ size_t offset = 12;
115
+ uint64_t total = 0;
116
+ uint64_t path_components = 0;
117
+ for (uint32_t index = 0; index < count; index++) {
118
+ if (offset + 8 > length) goto fail;
119
+ const uint32_t path_length = read_u32(buffer + offset);
120
+ const uint32_t content_length = read_u32(buffer + offset + 4);
121
+ offset += 8;
122
+ if (path_length == 0 || path_length > MAX_PATH_BYTES || content_length > MAX_FILE_BYTES ||
123
+ offset + (size_t)path_length + (size_t)content_length > length) goto fail;
124
+ files[index].path = malloc((size_t)path_length + 1);
125
+ if (!files[index].path) goto fail;
126
+ memcpy(files[index].path, buffer + offset, path_length);
127
+ files[index].path[path_length] = 0;
128
+ if (!canonical_path(files[index].path, path_length)) goto fail;
129
+ path_components++;
130
+ for (uint32_t byte = 0; byte < path_length; byte++) if (files[index].path[byte] == '/') path_components++;
131
+ if (path_components > MAX_PATH_COMPONENTS) goto fail;
132
+ offset += path_length;
133
+ files[index].content = buffer + offset;
134
+ files[index].size = content_length;
135
+ offset += content_length;
136
+ total += content_length;
137
+ if (total > MAX_TOTAL_BYTES) goto fail;
138
+ for (uint32_t prior = 0; prior < index; prior++) if (conflicting_paths(files[prior].path, files[index].path)) goto fail;
139
+ }
140
+ if (offset != length) goto fail;
141
+ *files_out = files;
142
+ *count_out = count;
143
+ return 1;
144
+ fail:
145
+ for (uint32_t index = 0; index < count; index++) free(files[index].path);
146
+ free(files);
147
+ return 0;
148
+ }
149
+
150
+ static int same_identity(const struct stat *left, const struct stat *right) {
151
+ return left->st_dev == right->st_dev && left->st_ino == right->st_ino;
152
+ }
153
+
154
+ static int open_root(const char *path, struct stat *identity) {
155
+ struct stat before, after, opened;
156
+ if (lstat(path, &before) != 0 || !S_ISDIR(before.st_mode) || S_ISLNK(before.st_mode)) return -1;
157
+ const int fd = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
158
+ if (fd < 0) return -1;
159
+ if (fstat(fd, &opened) != 0 || lstat(path, &after) != 0 || !S_ISDIR(after.st_mode) ||
160
+ !same_identity(&before, &opened) || !same_identity(&opened, &after)) { close(fd); return -1; }
161
+ *identity = opened;
162
+ return fd;
163
+ }
164
+
165
+ static void unlink_if_identity(int parent_fd, const char *name, const struct stat *identity, int flags) {
166
+ struct stat current;
167
+ if (fstatat(parent_fd, name, &current, AT_SYMLINK_NOFOLLOW) == 0 && same_identity(identity, &current)) {
168
+ unlinkat(parent_fd, name, flags);
169
+ }
170
+ }
171
+
172
+ static int add_created(CreatedEntry *entries, size_t *count, const char *path, const char *name,
173
+ const struct stat *stat, int parent_fd, int directory) {
174
+ if (*count >= MAX_CREATED_ENTRIES) return 0;
175
+ entries[*count].path = strdup(path);
176
+ entries[*count].name = strdup(name);
177
+ entries[*count].parent_fd = dup(parent_fd);
178
+ if (!entries[*count].path || !entries[*count].name || entries[*count].parent_fd < 0) {
179
+ free(entries[*count].path);
180
+ free(entries[*count].name);
181
+ if (entries[*count].parent_fd >= 0) close(entries[*count].parent_fd);
182
+ return 0;
183
+ }
184
+ entries[*count].dev = stat->st_dev;
185
+ entries[*count].ino = stat->st_ino;
186
+ entries[*count].directory = directory;
187
+ (*count)++;
188
+ return 1;
189
+ }
190
+
191
+ static int open_parent(int root_fd, const char *path, CreatedEntry *created, size_t *created_count, int create, char **name_out) {
192
+ char *copy = strdup(path);
193
+ char *prefix = calloc(strlen(path) + 1, 1);
194
+ if (!copy || !prefix) { free(copy); free(prefix); return -1; }
195
+ int current = dup(root_fd);
196
+ if (current < 0) { free(copy); free(prefix); return -1; }
197
+ char *cursor = copy;
198
+ char *slash;
199
+ while ((slash = strchr(cursor, '/')) != NULL) {
200
+ *slash = 0;
201
+ if (prefix[0]) strcat(prefix, "/");
202
+ strcat(prefix, cursor);
203
+ int next = openat(current, cursor, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
204
+ if (next < 0 && errno == ENOENT && create) {
205
+ if (mkdirat(current, cursor, 0700) != 0) goto fail;
206
+ struct stat created_stat;
207
+ if (fstatat(current, cursor, &created_stat, AT_SYMLINK_NOFOLLOW) != 0 || !S_ISDIR(created_stat.st_mode)) goto fail;
208
+ if (!add_created(created, created_count, prefix, cursor, &created_stat, current, 1)) {
209
+ unlink_if_identity(current, cursor, &created_stat, AT_REMOVEDIR);
210
+ goto fail;
211
+ }
212
+ next = openat(current, cursor, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
213
+ }
214
+ if (next < 0) goto fail;
215
+ struct stat stat;
216
+ if (fstat(next, &stat) != 0 || !S_ISDIR(stat.st_mode)) { close(next); goto fail; }
217
+ close(current);
218
+ current = next;
219
+ cursor = slash + 1;
220
+ }
221
+ *name_out = strdup(cursor);
222
+ free(copy);
223
+ free(prefix);
224
+ if (!*name_out) { close(current); return -1; }
225
+ return current;
226
+ fail:
227
+ close(current);
228
+ free(copy);
229
+ free(prefix);
230
+ return -1;
231
+ }
232
+
233
+ static int stat_relative(int root_fd, const char *path, struct stat *result, int *parent_out, char **name_out) {
234
+ CreatedEntry unused[1];
235
+ size_t unused_count = 0;
236
+ int parent = open_parent(root_fd, path, unused, &unused_count, 0, name_out);
237
+ if (parent < 0) return 0;
238
+ if (fstatat(parent, *name_out, result, AT_SYMLINK_NOFOLLOW) != 0) { close(parent); free(*name_out); return 0; }
239
+ *parent_out = parent;
240
+ return 1;
241
+ }
242
+
243
+ static void rollback(int root_fd, CreatedEntry *entries, size_t count) {
244
+ (void)root_fd;
245
+ while (count > 0) {
246
+ CreatedEntry *entry = &entries[--count];
247
+ const struct stat identity = { .st_dev = entry->dev, .st_ino = entry->ino };
248
+ unlink_if_identity(entry->parent_fd, entry->name, &identity, entry->directory ? AT_REMOVEDIR : 0);
249
+ }
250
+ }
251
+
252
+ static int verify_entries(int root_fd, CreatedEntry *entries, size_t count) {
253
+ for (size_t index = 0; index < count; index++) {
254
+ struct stat current;
255
+ int parent;
256
+ char *name = NULL;
257
+ if (!stat_relative(root_fd, entries[index].path, &current, &parent, &name)) return 0;
258
+ const int ok = current.st_dev == entries[index].dev && current.st_ino == entries[index].ino &&
259
+ (entries[index].directory ? S_ISDIR(current.st_mode) : S_ISREG(current.st_mode));
260
+ close(parent);
261
+ free(name);
262
+ if (!ok) return 0;
263
+ }
264
+ return 1;
265
+ }
266
+
267
+ static int extract_files(int root_fd, InputFile *files, uint32_t count, CreatedEntry *created, size_t *created_count) {
268
+ for (uint32_t index = 0; index < count; index++) {
269
+ char *name = NULL;
270
+ const int parent = open_parent(root_fd, files[index].path, created, created_count, 1, &name);
271
+ if (parent < 0) return 0;
272
+ const int fd = openat(parent, name, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
273
+ if (fd < 0) { close(parent); free(name); return 0; }
274
+ struct stat stat;
275
+ if (fstat(fd, &stat) != 0) {
276
+ close(fd); close(parent); free(name); return 0;
277
+ }
278
+ if (!S_ISREG(stat.st_mode)) {
279
+ close(fd); unlink_if_identity(parent, name, &stat, 0); close(parent); free(name); return 0;
280
+ }
281
+ if (!add_created(created, created_count, files[index].path, name, &stat, parent, 0)) {
282
+ close(fd); unlink_if_identity(parent, name, &stat, 0); close(parent); free(name); return 0;
283
+ }
284
+ size_t offset = 0;
285
+ while (offset < files[index].size) {
286
+ const ssize_t written = write(fd, files[index].content + offset, files[index].size - offset);
287
+ if (written < 0 && errno == EINTR) continue;
288
+ if (written <= 0) { close(fd); close(parent); free(name); return 0; }
289
+ offset += (size_t)written;
290
+ }
291
+ struct stat after;
292
+ const int ok = fstat(fd, &after) == 0 && same_identity(&stat, &after) && S_ISREG(after.st_mode);
293
+ close(fd);
294
+ close(parent);
295
+ free(name);
296
+ if (!ok) return 0;
297
+ }
298
+ return 1;
299
+ }
300
+
301
+ int main(int argc, char **argv) {
302
+ if (argc != 4) return 2;
303
+ char *dev_end = NULL;
304
+ char *ino_end = NULL;
305
+ const unsigned long long expected_dev = strtoull(argv[2], &dev_end, 10);
306
+ const unsigned long long expected_ino = strtoull(argv[3], &ino_end, 10);
307
+ if (!dev_end || *dev_end || !ino_end || *ino_end) return 2;
308
+ unsigned char *buffer = NULL;
309
+ size_t length = 0;
310
+ InputFile *files = NULL;
311
+ uint32_t file_count = 0;
312
+ if (!read_input(&buffer, &length) || !parse_input(buffer, length, &files, &file_count)) { free(buffer); return 2; }
313
+ CreatedEntry *created = calloc(MAX_CREATED_ENTRIES, sizeof(*created));
314
+ struct stat root_identity;
315
+ const int root_fd = created ? open_root(argv[1], &root_identity) : -1;
316
+ size_t created_count = 0;
317
+ int ok = root_fd >= 0 && (unsigned long long)root_identity.st_dev == expected_dev &&
318
+ (unsigned long long)root_identity.st_ino == expected_ino &&
319
+ extract_files(root_fd, files, file_count, created, &created_count) &&
320
+ verify_entries(root_fd, created, created_count);
321
+ struct stat root_after;
322
+ if (ok && (lstat(argv[1], &root_after) != 0 || !S_ISDIR(root_after.st_mode) || !same_identity(&root_identity, &root_after))) ok = 0;
323
+ if (!ok && root_fd >= 0) rollback(root_fd, created, created_count);
324
+ if (root_fd >= 0) close(root_fd);
325
+ for (size_t index = 0; index < created_count; index++) {
326
+ free(created[index].path);
327
+ free(created[index].name);
328
+ close(created[index].parent_fd);
329
+ }
330
+ for (uint32_t index = 0; index < file_count; index++) free(files[index].path);
331
+ free(created);
332
+ free(files);
333
+ free(buffer);
334
+ return ok ? 0 : 2;
335
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Secret redaction and scanning shared by artifact scanning, bundle building, and extraction.
3
+ */
4
+
5
+ import { isUtf8 } from "node:buffer";
6
+ import { TextDecoder } from "node:util";
7
+ import { scrubSensitiveText } from "../../shared/cursor-sensitive-text.mjs";
8
+
9
+ export const SECRET_PATTERNS = [
10
+ [/Authorization:\s*Bearer\s+[A-Za-z0-9\-._~+/]{20,}=*/gi, "Authorization header", "Authorization: Bearer [REDACTED_BEARER_TOKEN]"],
11
+ [/(bearer\s+)[A-Za-z0-9\-._~+/]{20,}=*/gi, "bearer token", "$1[REDACTED_BEARER_TOKEN]"],
12
+ [/connect\.sid=[A-Za-z0-9%]+/gi, "session cookie", "connect.sid=[REDACTED_SESSION_COOKIE]"],
13
+ [/https?:\/\/[^/\s]*\/cursor-pi-tool-bridge\/[A-Za-z0-9_.:-]+\/mcp/gi, "bridge endpoint URL", "[REDACTED_BRIDGE_ENDPOINT_URL]"],
14
+ [/"(apiKey|accessToken|refreshToken|session|cookie)"\s*:\s*"[^"\s]{12,}"/gi, "auth/token JSON field", '"$1":"[REDACTED_SECRET]"'],
15
+ [/\b[a-z][a-z0-9+.-]*:\/\/(?!\[redacted\]@)[^\s/?#]*@[^\s/?#]+/gi, "credential-bearing URL", "[REDACTED_CREDENTIAL_URL]"],
16
+ [/\b[^\s/:@]+:[^\s/]+@(?=[A-Za-z0-9.-]+(?::|\/|\s|$))/g, "credential-bearing SCP URL", "[REDACTED_CREDENTIAL_URL]"],
17
+ ];
18
+
19
+ const AUTH_ASSIGNMENT_PATTERN = /(?:^|[^A-Za-z0-9_$])(?:authorization|api[_-]?key|apiKey|access[_-]?token|refresh[_-]?token|token)["']?\s*[:=]\s*/gim;
20
+ const UNQUOTED_SECRET_CHARACTER = /^[A-Za-z0-9._~+/=-]$/;
21
+
22
+ function authAssignmentSecretRanges(text) {
23
+ const ranges = [];
24
+ AUTH_ASSIGNMENT_PATTERN.lastIndex = 0;
25
+ let match;
26
+ while ((match = AUTH_ASSIGNMENT_PATTERN.exec(text))) {
27
+ let start = AUTH_ASSIGNMENT_PATTERN.lastIndex;
28
+ const quote = text[start];
29
+ if (quote === '"' || quote === "'" || quote === "`") {
30
+ let end = start + 1;
31
+ while (end < text.length && text[end] !== quote) {
32
+ end += text[end] === "\\" && end + 1 < text.length ? 2 : 1;
33
+ }
34
+ const sourceBearerTemplate = quote === "`" && /^Bearer\s+\$\{[A-Za-z_$][A-Za-z0-9_$.[\]]*\}$/.test(text.slice(start + 1, end));
35
+ if (!sourceBearerTemplate && end - start - 1 >= 12) ranges.push({ start: start + 1, end });
36
+ if (end >= text.length) break;
37
+ AUTH_ASSIGNMENT_PATTERN.lastIndex = end + 1;
38
+ continue;
39
+ }
40
+ let hasDigit = false;
41
+ let end = start;
42
+ while (end < text.length && UNQUOTED_SECRET_CHARACTER.test(text[end])) {
43
+ hasDigit ||= /[0-9]/.test(text[end]);
44
+ end++;
45
+ }
46
+ if (hasDigit && end - start >= 12) ranges.push({ start, end });
47
+ AUTH_ASSIGNMENT_PATTERN.lastIndex = Math.max(end, start + 1);
48
+ }
49
+ return ranges;
50
+ }
51
+
52
+ function redactAuthAssignments(text) {
53
+ let redacted = text;
54
+ for (const range of authAssignmentSecretRanges(text).reverse()) {
55
+ redacted = `${redacted.slice(0, range.start)}[REDACTED_SECRET]${redacted.slice(range.end)}`;
56
+ }
57
+ return redacted;
58
+ }
59
+
60
+ /** Redact known secret material before writing logs/artifacts. */
61
+ export function redactSecrets(text) {
62
+ const cursorKey = process.env.CURSOR_API_KEY;
63
+ let redacted = redactAuthAssignments(String(text ?? ""));
64
+ redacted = scrubSensitiveText(redacted, cursorKey);
65
+ if (cursorKey && cursorKey.length > 10) {
66
+ redacted = redacted.split(cursorKey).join("[REDACTED_CURSOR_API_KEY]");
67
+ }
68
+ for (const [pattern, , replacement] of SECRET_PATTERNS) {
69
+ redacted = redacted.replace(pattern, replacement);
70
+ }
71
+ return redacted;
72
+ }
73
+
74
+ export function isBinaryArtifactContent(value) {
75
+ return Buffer.isBuffer(value) && (!isUtf8(value) || value.includes(0));
76
+ }
77
+
78
+ function swapUtf16Bytes(value) {
79
+ const result = Buffer.from(value);
80
+ for (let index = 0; index + 1 < result.length; index += 2) {
81
+ [result[index], result[index + 1]] = [result[index + 1], result[index]];
82
+ }
83
+ return result;
84
+ }
85
+
86
+ function encodeUtf32(value, littleEndian) {
87
+ const codePoints = [...value].map((character) => character.codePointAt(0));
88
+ const result = Buffer.alloc(codePoints.length * 4);
89
+ for (let index = 0; index < codePoints.length; index++) {
90
+ if (littleEndian) result.writeUInt32LE(codePoints[index], index * 4);
91
+ else result.writeUInt32BE(codePoints[index], index * 4);
92
+ }
93
+ return result;
94
+ }
95
+
96
+ function decodeUtf32(value, littleEndian) {
97
+ let result = "";
98
+ const chunk = [];
99
+ for (let offset = 0; offset + 3 < value.length; offset += 4) {
100
+ const codePoint = littleEndian ? value.readUInt32LE(offset) : value.readUInt32BE(offset);
101
+ chunk.push(codePoint <= 0x10ffff && (codePoint < 0xd800 || codePoint > 0xdfff) ? String.fromCodePoint(codePoint) : "\ufffd");
102
+ if (chunk.length === 4096) {
103
+ result += chunk.join("");
104
+ chunk.length = 0;
105
+ }
106
+ }
107
+ return result + chunk.join("");
108
+ }
109
+
110
+ function* binaryTextCandidates(value) {
111
+ yield value.toString("latin1");
112
+ for (let offset = 0; offset < Math.min(2, value.length); offset++) {
113
+ const aligned = value.subarray(offset);
114
+ yield new TextDecoder("utf-16le").decode(aligned);
115
+ yield new TextDecoder("utf-16be").decode(aligned);
116
+ }
117
+ for (let offset = 0; offset < Math.min(4, value.length); offset++) {
118
+ const aligned = value.subarray(offset);
119
+ yield decodeUtf32(aligned, true);
120
+ yield decodeUtf32(aligned, false);
121
+ }
122
+ }
123
+
124
+ function bufferContainsEncodedSecret(value, secret) {
125
+ const utf16Le = Buffer.from(secret, "utf16le");
126
+ return [
127
+ Buffer.from(secret, "utf8"), utf16Le, swapUtf16Bytes(utf16Le),
128
+ encodeUtf32(secret, true), encodeUtf32(secret, false),
129
+ ].some((encoded) => encoded.length > 0 && value.indexOf(encoded) !== -1);
130
+ }
131
+
132
+ /** Scan text or binary content for secrets. Returns array of violation descriptions. */
133
+ export function scanForSecrets(value) {
134
+ const buffer = Buffer.isBuffer(value) ? value : undefined;
135
+ const binary = isBinaryArtifactContent(value);
136
+ const plainText = buffer ? undefined : String(value ?? "");
137
+ const violations = new Set();
138
+ const cursorKey = process.env.CURSOR_API_KEY;
139
+ if (cursorKey && cursorKey.length > 10 && (
140
+ buffer ? bufferContainsEncodedSecret(buffer, cursorKey) : plainText.includes(cursorKey)
141
+ )) {
142
+ violations.add("CURSOR_API_KEY literal found");
143
+ }
144
+ const patterns = SECRET_PATTERNS.filter(([, label]) =>
145
+ !binary || (label !== "credential-bearing URL" && label !== "credential-bearing SCP URL"));
146
+ const texts = buffer ? (binary ? binaryTextCandidates(buffer) : [buffer.toString("utf8")]) : [plainText];
147
+ for (const text of texts) {
148
+ if (authAssignmentSecretRanges(text).length > 0) violations.add("potential auth/token assignment");
149
+ for (const [pattern, label] of patterns) {
150
+ pattern.lastIndex = 0;
151
+ if (pattern.test(text)) violations.add(`potential ${label}`);
152
+ }
153
+ }
154
+ return [...violations];
155
+ }