@skrr-ai/cli 0.1.102 → 0.1.104

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 (52) hide show
  1. package/dist/base-command.js +24 -0
  2. package/dist/commands/agent/message/send.js +6 -2
  3. package/dist/commands/agents/triggers/create.js +7 -17
  4. package/dist/commands/agents/triggers/update.js +41 -28
  5. package/dist/commands/commitments/create.js +2 -0
  6. package/dist/commands/commitments/preflight.js +18 -1
  7. package/dist/commands/commitments/roster.d.ts +15 -0
  8. package/dist/commands/commitments/roster.js +39 -0
  9. package/dist/commands/commitments/show.js +1 -1
  10. package/dist/commands/commitments/status.d.ts +17 -0
  11. package/dist/commands/commitments/status.js +41 -0
  12. package/dist/commands/commitments/update.js +12 -0
  13. package/dist/commands/daemon/logs.d.ts +106 -1
  14. package/dist/commands/daemon/logs.js +311 -15
  15. package/dist/commands/followups/checkin.js +46 -3
  16. package/dist/commands/tasks/approvals/resolve.d.ts +12 -0
  17. package/dist/commands/tasks/approvals/resolve.js +21 -5
  18. package/dist/commands/triggers/create.js +8 -17
  19. package/dist/commands/triggers/fire.js +3 -0
  20. package/dist/commands/triggers/update.js +41 -27
  21. package/dist/commands/widgets/check.d.ts +39 -0
  22. package/dist/commands/widgets/check.js +179 -0
  23. package/dist/lib/agent-mailbox.d.ts +0 -1
  24. package/dist/lib/agent-mailbox.js +0 -1
  25. package/dist/lib/commitment-loop-status.d.ts +16 -0
  26. package/dist/lib/commitment-loop-status.js +164 -0
  27. package/dist/lib/commitments.d.ts +28 -1
  28. package/dist/lib/commitments.js +184 -5
  29. package/dist/lib/delegated-cli.d.ts +23 -1
  30. package/dist/lib/delegated-cli.js +61 -4
  31. package/dist/lib/first-party-harness.d.ts +1 -1
  32. package/dist/lib/first-party-harness.js +1 -1
  33. package/dist/lib/session-task-write.d.ts +13 -0
  34. package/dist/lib/session-task-write.js +63 -3
  35. package/dist/lib/triggers.d.ts +26 -0
  36. package/dist/lib/triggers.js +134 -1
  37. package/dist/lib/widgetCheck.d.ts +123 -0
  38. package/dist/lib/widgetCheck.js +998 -0
  39. package/dist/lib/widgetStaticScan.d.ts +111 -0
  40. package/dist/lib/widgetStaticScan.js +833 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +9 -1
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +12 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +61 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +177 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +9 -1
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +12 -0
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +61 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +171 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  50. package/dist/node_modules/@skrr-ai/data-provider/index.js +11908 -8693
  51. package/oclif.manifest.json +219 -6
  52. package/package.json +2 -2
@@ -10,7 +10,7 @@ type DeviceIdentityState = {
10
10
  profile: string;
11
11
  envelopeProtected: boolean;
12
12
  };
13
- type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key';
13
+ type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key' | 'no_stored_key';
14
14
  /** @internal Test seam — force-set state. */
15
15
  export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
16
16
  /** @internal Test seam — read state. */
@@ -47,9 +47,17 @@ export declare function describeDeviceKeySplit(profile: string): {
47
47
  * next refresh with a key the server never pinned.
48
48
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
49
49
  * error landing in disabled+persist_failed.
50
+ *
51
+ * `createIfMissing: false` stops before step 2 (disabled+no_stored_key) and
52
+ * never replaces anything: for a short-lived process that must only READ the
53
+ * daemon's key while the daemon itself may be creating it (OSK-15990's
54
+ * `skrrd device-proof`). The persist below is not an exclusive create, so a
55
+ * second creator racing the daemon could leave the daemon holding one key in
56
+ * memory and the disk another.
50
57
  */
51
58
  export declare function initDeviceIdentity(profile: string, options?: {
52
59
  recoverUnreadable?: boolean;
60
+ createIfMissing?: boolean;
53
61
  }): Promise<void>;
54
62
  /**
55
63
  * Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
@@ -364,6 +364,13 @@ async function consolidateDeviceIdentity(profile) {
364
364
  * next refresh with a key the server never pinned.
365
365
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
366
366
  * error landing in disabled+persist_failed.
367
+ *
368
+ * `createIfMissing: false` stops before step 2 (disabled+no_stored_key) and
369
+ * never replaces anything: for a short-lived process that must only READ the
370
+ * daemon's key while the daemon itself may be creating it (OSK-15990's
371
+ * `skrrd device-proof`). The persist below is not an exclusive create, so a
372
+ * second creator racing the daemon could leave the daemon holding one key in
373
+ * memory and the disk another.
367
374
  */
368
375
  async function initDeviceIdentity(profile, options = {}) {
369
376
  if (_state.kind !== 'uninit')
@@ -412,6 +419,11 @@ async function initDeviceIdentity(profile, options = {}) {
412
419
  emit('recovery_required', false, { profile, reason: 'stored_key_unreadable' });
413
420
  return;
414
421
  }
422
+ if (options.createIfMissing === false) {
423
+ _state = { kind: 'disabled', reason: 'no_stored_key' };
424
+ emit('no_stored_key', false, { profile });
425
+ return;
426
+ }
415
427
  let fresh;
416
428
  try {
417
429
  fresh = (0, deviceKey_js_1.generateDeviceKeyPair)();
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Keeping a Hosted Machine preview capability out of telemetry (OSK-16174).
3
+ *
4
+ * `/api/previews/<token>/...` serves a person's machine with NO login: the token in the path is a bearer
5
+ * capability (`pvw_<22 chars>.<expiry>.<22 chars>`, idle TTL ten minutes). Anyone who can read a trace, an error
6
+ * report or a log line that carries it can open the preview, so every sink that records a URL has to run its
7
+ * strings through here first. This file is the ONE place that knows what to look for; the sinks (OpenTelemetry
8
+ * exporters, Sentry hooks, request and CSRF logs, client-log ingest) only decide WHERE to call it.
9
+ *
10
+ * WHAT IT MATCHES. The prefix `pvw_` and then every character a token is made of, however far that goes, rather
11
+ * than the exact 22.digits.22 shape. A truncated token is still part of the secret, a log line cut at 200
12
+ * characters is the ordinary way one gets truncated, and a looser match costs nothing: the marker replaces
13
+ * only what looked like a token. The token alphabet is base64url plus the two dots, or any of those characters
14
+ * written as a percent-escape, once or several times (`encodeURIComponent` of a URL that was itself in a query
15
+ * string). `packages/data-provider/specs/hostedPreview.spec.ts` pins this against the contract's token shape
16
+ * and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
17
+ * mints, so a change to either side fails there rather than in a trace nobody reads.
18
+ *
19
+ * WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
20
+ * repository writes one, and a scan that decoded every string first would cost every sink on every record. The
21
+ * request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
22
+ * stays as it is.
23
+ *
24
+ * COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
25
+ * cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
26
+ *
27
+ * NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
28
+ * with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
29
+ * record, because a record with the token still in it is exactly what this exists to prevent.
30
+ */
31
+ /** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
32
+ export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
33
+ /**
34
+ * Replace every preview token in a string with {@link PREVIEW_TOKEN_REDACTION}. The rest of the string is
35
+ * untouched, so a trace is still readable. Anything that is not a string comes back as it came.
36
+ */
37
+ export declare function redactPreviewTokens(value: string): string;
38
+ export declare function redactPreviewTokens<T>(value: T): T;
39
+ export interface PreviewTokenWalkLimits {
40
+ /** Objects and arrays visited before the walk gives up. Default 50 000: a Sentry transaction of a thousand spans. */
41
+ maxNodes?: number;
42
+ /** Nesting followed before the walk gives up. Default 32. */
43
+ maxDepth?: number;
44
+ }
45
+ /**
46
+ * Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
47
+ * copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
48
+ * the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
49
+ * be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
50
+ * not enumerable.
51
+ */
52
+ export declare function redactPreviewTokensDeep(root: unknown, limits?: PreviewTokenWalkLimits): boolean;
53
+ /**
54
+ * Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
55
+ * are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
56
+ * `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
57
+ * An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
58
+ * what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
59
+ * Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
60
+ */
61
+ export declare function withPreviewTokenRedaction<T extends object>(options: T): T;
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ /**
3
+ * Keeping a Hosted Machine preview capability out of telemetry (OSK-16174).
4
+ *
5
+ * `/api/previews/<token>/...` serves a person's machine with NO login: the token in the path is a bearer
6
+ * capability (`pvw_<22 chars>.<expiry>.<22 chars>`, idle TTL ten minutes). Anyone who can read a trace, an error
7
+ * report or a log line that carries it can open the preview, so every sink that records a URL has to run its
8
+ * strings through here first. This file is the ONE place that knows what to look for; the sinks (OpenTelemetry
9
+ * exporters, Sentry hooks, request and CSRF logs, client-log ingest) only decide WHERE to call it.
10
+ *
11
+ * WHAT IT MATCHES. The prefix `pvw_` and then every character a token is made of, however far that goes, rather
12
+ * than the exact 22.digits.22 shape. A truncated token is still part of the secret, a log line cut at 200
13
+ * characters is the ordinary way one gets truncated, and a looser match costs nothing: the marker replaces
14
+ * only what looked like a token. The token alphabet is base64url plus the two dots, or any of those characters
15
+ * written as a percent-escape, once or several times (`encodeURIComponent` of a URL that was itself in a query
16
+ * string). `packages/data-provider/specs/hostedPreview.spec.ts` pins this against the contract's token shape
17
+ * and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
18
+ * mints, so a change to either side fails there rather than in a trace nobody reads.
19
+ *
20
+ * WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
21
+ * repository writes one, and a scan that decoded every string first would cost every sink on every record. The
22
+ * request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
23
+ * stays as it is.
24
+ *
25
+ * COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
26
+ * cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
27
+ *
28
+ * NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
29
+ * with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
30
+ * record, because a record with the token still in it is exactly what this exists to prevent.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.PREVIEW_TOKEN_REDACTION = void 0;
34
+ exports.redactPreviewTokens = redactPreviewTokens;
35
+ exports.redactPreviewTokensDeep = redactPreviewTokensDeep;
36
+ exports.withPreviewTokenRedaction = withPreviewTokenRedaction;
37
+ /** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
38
+ exports.PREVIEW_TOKEN_REDACTION = 'pvw_[redacted]';
39
+ /**
40
+ * One character of a token: base64url, a dot, or any of those written as a percent-escape. An escape that was
41
+ * escaped again is `%25` followed by the original two digits (`_` is `%5F`, then `%255F`, then `%25255F`), which is
42
+ * what the `(?:25)*` is for. `25` is not itself a digit pair of a token character, so a run of them can only be
43
+ * read one way.
44
+ */
45
+ const TOKEN_CHAR = '(?:[A-Za-z0-9._-]|%(?:25)*(?:2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[0-9AaFf]|6[1-9A-Fa-f]|7[0-9Aa]))';
46
+ /**
47
+ * `pvw` then an underscore, which may be written `%5F` (or `%255F`, ...); then at least ONE token character. Not
48
+ * zero: the marker is `pvw_[`, and `[` is not a token character, so a string that was already redacted matches
49
+ * nothing and redacting twice changes nothing.
50
+ */
51
+ const PREVIEW_TOKEN = new RegExp(`pvw(?:_|%(?:25)*5[Ff])${TOKEN_CHAR}+`, 'g');
52
+ function redactPreviewTokens(value) {
53
+ if (typeof value !== 'string' || value.indexOf('pvw') === -1) {
54
+ return value;
55
+ }
56
+ return value.replace(PREVIEW_TOKEN, exports.PREVIEW_TOKEN_REDACTION);
57
+ }
58
+ const DEFAULT_MAX_NODES = 50_000;
59
+ const DEFAULT_MAX_DEPTH = 32;
60
+ /**
61
+ * Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
62
+ * copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
63
+ * the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
64
+ * be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
65
+ * not enumerable.
66
+ */
67
+ function redactPreviewTokensDeep(root, limits = {}) {
68
+ if (root === null || typeof root !== 'object') {
69
+ return true;
70
+ }
71
+ const maxNodes = limits.maxNodes ?? DEFAULT_MAX_NODES;
72
+ const maxDepth = limits.maxDepth ?? DEFAULT_MAX_DEPTH;
73
+ try {
74
+ const seen = new WeakSet();
75
+ const stack = [[root, 0]];
76
+ let visited = 0;
77
+ let complete = true;
78
+ const rewrite = (holder, key, current) => {
79
+ const next = redactPreviewTokens(current);
80
+ if (next === current) {
81
+ return;
82
+ }
83
+ try {
84
+ holder[key] = next;
85
+ }
86
+ catch {
87
+ // a frozen object throws in strict code
88
+ }
89
+ if (holder[key] !== next) {
90
+ // and silently keeps the old value in sloppy code
91
+ complete = false;
92
+ }
93
+ };
94
+ while (stack.length > 0) {
95
+ const [node, depth] = stack.pop();
96
+ if (seen.has(node) || ArrayBuffer.isView(node)) {
97
+ continue;
98
+ }
99
+ seen.add(node);
100
+ visited += 1;
101
+ if (visited > maxNodes) {
102
+ return false;
103
+ }
104
+ const holder = node;
105
+ if (node instanceof Error) {
106
+ for (const key of ['message', 'stack']) {
107
+ if (typeof holder[key] === 'string') {
108
+ rewrite(holder, key, holder[key]);
109
+ }
110
+ }
111
+ }
112
+ for (const key of Object.keys(node)) {
113
+ let child;
114
+ try {
115
+ child = holder[key];
116
+ }
117
+ catch {
118
+ complete = false;
119
+ continue;
120
+ }
121
+ if (typeof child === 'string') {
122
+ rewrite(holder, key, child);
123
+ }
124
+ else if (child !== null && typeof child === 'object') {
125
+ if (depth + 1 > maxDepth) {
126
+ complete = false;
127
+ }
128
+ else {
129
+ stack.push([child, depth + 1]);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return complete;
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ function isThenable(value) {
141
+ return (value !== null &&
142
+ (typeof value === 'object' || typeof value === 'function') &&
143
+ typeof value.then === 'function');
144
+ }
145
+ /** The record with its tokens redacted, or `null` (dropped) when that could not be done. */
146
+ function scrubRecord(record) {
147
+ if (record === null || record === undefined) {
148
+ return record;
149
+ }
150
+ if (typeof record !== 'object') {
151
+ return null;
152
+ }
153
+ return redactPreviewTokensDeep(record) ? record : null;
154
+ }
155
+ function guard(existing) {
156
+ return (value, hint) => {
157
+ const result = existing ? existing(value, hint) : value;
158
+ return isThenable(result) ? result.then(scrubRecord) : scrubRecord(result);
159
+ };
160
+ }
161
+ /**
162
+ * Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
163
+ * are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
164
+ * `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
165
+ * An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
166
+ * what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
167
+ * Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
168
+ */
169
+ function withPreviewTokenRedaction(options) {
170
+ const hooks = options;
171
+ return {
172
+ ...options,
173
+ beforeSend: guard(hooks.beforeSend),
174
+ beforeSendTransaction: guard(hooks.beforeSendTransaction),
175
+ beforeBreadcrumb: guard(hooks.beforeBreadcrumb),
176
+ };
177
+ }
@@ -10,7 +10,7 @@ type DeviceIdentityState = {
10
10
  profile: string;
11
11
  envelopeProtected: boolean;
12
12
  };
13
- type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key';
13
+ type DisabledReason = 'init_failed' | 'load_failed' | 'persist_failed' | 'unreadable_key' | 'no_stored_key';
14
14
  /** @internal Test seam — force-set state. */
15
15
  export declare function __setStateForTest(next: DeviceIdentityState): DeviceIdentityState;
16
16
  /** @internal Test seam — read state. */
@@ -47,9 +47,17 @@ export declare function describeDeviceKeySplit(profile: string): {
47
47
  * next refresh with a key the server never pinned.
48
48
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
49
49
  * error landing in disabled+persist_failed.
50
+ *
51
+ * `createIfMissing: false` stops before step 2 (disabled+no_stored_key) and
52
+ * never replaces anything: for a short-lived process that must only READ the
53
+ * daemon's key while the daemon itself may be creating it (OSK-15990's
54
+ * `skrrd device-proof`). The persist below is not an exclusive create, so a
55
+ * second creator racing the daemon could leave the daemon holding one key in
56
+ * memory and the disk another.
50
57
  */
51
58
  export declare function initDeviceIdentity(profile: string, options?: {
52
59
  recoverUnreadable?: boolean;
60
+ createIfMissing?: boolean;
53
61
  }): Promise<void>;
54
62
  /**
55
63
  * Reset in-memory state. With `clearOnDisk=true` deletes both keypair files.
@@ -344,6 +344,13 @@ async function consolidateDeviceIdentity(profile) {
344
344
  * next refresh with a key the server never pinned.
345
345
  * 2. Generate fresh keypair and persist. Failure to persist is a hard
346
346
  * error landing in disabled+persist_failed.
347
+ *
348
+ * `createIfMissing: false` stops before step 2 (disabled+no_stored_key) and
349
+ * never replaces anything: for a short-lived process that must only READ the
350
+ * daemon's key while the daemon itself may be creating it (OSK-15990's
351
+ * `skrrd device-proof`). The persist below is not an exclusive create, so a
352
+ * second creator racing the daemon could leave the daemon holding one key in
353
+ * memory and the disk another.
347
354
  */
348
355
  export async function initDeviceIdentity(profile, options = {}) {
349
356
  if (_state.kind !== 'uninit')
@@ -392,6 +399,11 @@ export async function initDeviceIdentity(profile, options = {}) {
392
399
  emit('recovery_required', false, { profile, reason: 'stored_key_unreadable' });
393
400
  return;
394
401
  }
402
+ if (options.createIfMissing === false) {
403
+ _state = { kind: 'disabled', reason: 'no_stored_key' };
404
+ emit('no_stored_key', false, { profile });
405
+ return;
406
+ }
395
407
  let fresh;
396
408
  try {
397
409
  fresh = generateDeviceKeyPair();
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Keeping a Hosted Machine preview capability out of telemetry (OSK-16174).
3
+ *
4
+ * `/api/previews/<token>/...` serves a person's machine with NO login: the token in the path is a bearer
5
+ * capability (`pvw_<22 chars>.<expiry>.<22 chars>`, idle TTL ten minutes). Anyone who can read a trace, an error
6
+ * report or a log line that carries it can open the preview, so every sink that records a URL has to run its
7
+ * strings through here first. This file is the ONE place that knows what to look for; the sinks (OpenTelemetry
8
+ * exporters, Sentry hooks, request and CSRF logs, client-log ingest) only decide WHERE to call it.
9
+ *
10
+ * WHAT IT MATCHES. The prefix `pvw_` and then every character a token is made of, however far that goes, rather
11
+ * than the exact 22.digits.22 shape. A truncated token is still part of the secret, a log line cut at 200
12
+ * characters is the ordinary way one gets truncated, and a looser match costs nothing: the marker replaces
13
+ * only what looked like a token. The token alphabet is base64url plus the two dots, or any of those characters
14
+ * written as a percent-escape, once or several times (`encodeURIComponent` of a URL that was itself in a query
15
+ * string). `packages/data-provider/specs/hostedPreview.spec.ts` pins this against the contract's token shape
16
+ * and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
17
+ * mints, so a change to either side fails there rather than in a trace nobody reads.
18
+ *
19
+ * WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
20
+ * repository writes one, and a scan that decoded every string first would cost every sink on every record. The
21
+ * request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
22
+ * stays as it is.
23
+ *
24
+ * COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
25
+ * cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
26
+ *
27
+ * NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
28
+ * with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
29
+ * record, because a record with the token still in it is exactly what this exists to prevent.
30
+ */
31
+ /** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
32
+ export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
33
+ /**
34
+ * Replace every preview token in a string with {@link PREVIEW_TOKEN_REDACTION}. The rest of the string is
35
+ * untouched, so a trace is still readable. Anything that is not a string comes back as it came.
36
+ */
37
+ export declare function redactPreviewTokens(value: string): string;
38
+ export declare function redactPreviewTokens<T>(value: T): T;
39
+ export interface PreviewTokenWalkLimits {
40
+ /** Objects and arrays visited before the walk gives up. Default 50 000: a Sentry transaction of a thousand spans. */
41
+ maxNodes?: number;
42
+ /** Nesting followed before the walk gives up. Default 32. */
43
+ maxDepth?: number;
44
+ }
45
+ /**
46
+ * Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
47
+ * copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
48
+ * the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
49
+ * be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
50
+ * not enumerable.
51
+ */
52
+ export declare function redactPreviewTokensDeep(root: unknown, limits?: PreviewTokenWalkLimits): boolean;
53
+ /**
54
+ * Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
55
+ * are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
56
+ * `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
57
+ * An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
58
+ * what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
59
+ * Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
60
+ */
61
+ export declare function withPreviewTokenRedaction<T extends object>(options: T): T;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Keeping a Hosted Machine preview capability out of telemetry (OSK-16174).
3
+ *
4
+ * `/api/previews/<token>/...` serves a person's machine with NO login: the token in the path is a bearer
5
+ * capability (`pvw_<22 chars>.<expiry>.<22 chars>`, idle TTL ten minutes). Anyone who can read a trace, an error
6
+ * report or a log line that carries it can open the preview, so every sink that records a URL has to run its
7
+ * strings through here first. This file is the ONE place that knows what to look for; the sinks (OpenTelemetry
8
+ * exporters, Sentry hooks, request and CSRF logs, client-log ingest) only decide WHERE to call it.
9
+ *
10
+ * WHAT IT MATCHES. The prefix `pvw_` and then every character a token is made of, however far that goes, rather
11
+ * than the exact 22.digits.22 shape. A truncated token is still part of the secret, a log line cut at 200
12
+ * characters is the ordinary way one gets truncated, and a looser match costs nothing: the marker replaces
13
+ * only what looked like a token. The token alphabet is base64url plus the two dots, or any of those characters
14
+ * written as a percent-escape, once or several times (`encodeURIComponent` of a URL that was itself in a query
15
+ * string). `packages/data-provider/specs/hostedPreview.spec.ts` pins this against the contract's token shape
16
+ * and `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` against tokens the issuer really
17
+ * mints, so a change to either side fails there rather than in a trace nobody reads.
18
+ *
19
+ * WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
20
+ * repository writes one, and a scan that decoded every string first would cost every sink on every record. The
21
+ * request log's own `/[credential]` rewrite (`requestLogger.js`) covers the other credential prefixes and
22
+ * stays as it is.
23
+ *
24
+ * COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
25
+ * cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
26
+ *
27
+ * NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
28
+ * with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
29
+ * record, because a record with the token still in it is exactly what this exists to prevent.
30
+ */
31
+ /** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
32
+ export const PREVIEW_TOKEN_REDACTION = 'pvw_[redacted]';
33
+ /**
34
+ * One character of a token: base64url, a dot, or any of those written as a percent-escape. An escape that was
35
+ * escaped again is `%25` followed by the original two digits (`_` is `%5F`, then `%255F`, then `%25255F`), which is
36
+ * what the `(?:25)*` is for. `25` is not itself a digit pair of a token character, so a run of them can only be
37
+ * read one way.
38
+ */
39
+ const TOKEN_CHAR = '(?:[A-Za-z0-9._-]|%(?:25)*(?:2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[0-9AaFf]|6[1-9A-Fa-f]|7[0-9Aa]))';
40
+ /**
41
+ * `pvw` then an underscore, which may be written `%5F` (or `%255F`, ...); then at least ONE token character. Not
42
+ * zero: the marker is `pvw_[`, and `[` is not a token character, so a string that was already redacted matches
43
+ * nothing and redacting twice changes nothing.
44
+ */
45
+ const PREVIEW_TOKEN = new RegExp(`pvw(?:_|%(?:25)*5[Ff])${TOKEN_CHAR}+`, 'g');
46
+ export function redactPreviewTokens(value) {
47
+ if (typeof value !== 'string' || value.indexOf('pvw') === -1) {
48
+ return value;
49
+ }
50
+ return value.replace(PREVIEW_TOKEN, PREVIEW_TOKEN_REDACTION);
51
+ }
52
+ const DEFAULT_MAX_NODES = 50_000;
53
+ const DEFAULT_MAX_DEPTH = 32;
54
+ /**
55
+ * Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
56
+ * copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
57
+ * the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
58
+ * be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
59
+ * not enumerable.
60
+ */
61
+ export function redactPreviewTokensDeep(root, limits = {}) {
62
+ if (root === null || typeof root !== 'object') {
63
+ return true;
64
+ }
65
+ const maxNodes = limits.maxNodes ?? DEFAULT_MAX_NODES;
66
+ const maxDepth = limits.maxDepth ?? DEFAULT_MAX_DEPTH;
67
+ try {
68
+ const seen = new WeakSet();
69
+ const stack = [[root, 0]];
70
+ let visited = 0;
71
+ let complete = true;
72
+ const rewrite = (holder, key, current) => {
73
+ const next = redactPreviewTokens(current);
74
+ if (next === current) {
75
+ return;
76
+ }
77
+ try {
78
+ holder[key] = next;
79
+ }
80
+ catch {
81
+ // a frozen object throws in strict code
82
+ }
83
+ if (holder[key] !== next) {
84
+ // and silently keeps the old value in sloppy code
85
+ complete = false;
86
+ }
87
+ };
88
+ while (stack.length > 0) {
89
+ const [node, depth] = stack.pop();
90
+ if (seen.has(node) || ArrayBuffer.isView(node)) {
91
+ continue;
92
+ }
93
+ seen.add(node);
94
+ visited += 1;
95
+ if (visited > maxNodes) {
96
+ return false;
97
+ }
98
+ const holder = node;
99
+ if (node instanceof Error) {
100
+ for (const key of ['message', 'stack']) {
101
+ if (typeof holder[key] === 'string') {
102
+ rewrite(holder, key, holder[key]);
103
+ }
104
+ }
105
+ }
106
+ for (const key of Object.keys(node)) {
107
+ let child;
108
+ try {
109
+ child = holder[key];
110
+ }
111
+ catch {
112
+ complete = false;
113
+ continue;
114
+ }
115
+ if (typeof child === 'string') {
116
+ rewrite(holder, key, child);
117
+ }
118
+ else if (child !== null && typeof child === 'object') {
119
+ if (depth + 1 > maxDepth) {
120
+ complete = false;
121
+ }
122
+ else {
123
+ stack.push([child, depth + 1]);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return complete;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ function isThenable(value) {
135
+ return (value !== null &&
136
+ (typeof value === 'object' || typeof value === 'function') &&
137
+ typeof value.then === 'function');
138
+ }
139
+ /** The record with its tokens redacted, or `null` (dropped) when that could not be done. */
140
+ function scrubRecord(record) {
141
+ if (record === null || record === undefined) {
142
+ return record;
143
+ }
144
+ if (typeof record !== 'object') {
145
+ return null;
146
+ }
147
+ return redactPreviewTokensDeep(record) ? record : null;
148
+ }
149
+ function guard(existing) {
150
+ return (value, hint) => {
151
+ const result = existing ? existing(value, hint) : value;
152
+ return isThenable(result) ? result.then(scrubRecord) : scrubRecord(result);
153
+ };
154
+ }
155
+ /**
156
+ * Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
157
+ * are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
158
+ * `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
159
+ * An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
160
+ * what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
161
+ * Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
162
+ */
163
+ export function withPreviewTokenRedaction(options) {
164
+ const hooks = options;
165
+ return {
166
+ ...options,
167
+ beforeSend: guard(hooks.beforeSend),
168
+ beforeSendTransaction: guard(hooks.beforeSendTransaction),
169
+ beforeBreadcrumb: guard(hooks.beforeBreadcrumb),
170
+ };
171
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skrr-ai/auth-core",
3
- "version": "0.4.21",
3
+ "version": "0.4.23",
4
4
  "main": "dist/cjs/index.js",
5
5
  "types": "dist/esm/index.d.ts",
6
6
  "exports": {
@@ -183,6 +183,16 @@
183
183
  "types": "./dist/cjs/releasePolicy.d.ts",
184
184
  "default": "./dist/cjs/releasePolicy.js"
185
185
  }
186
+ },
187
+ "./telemetry-redaction": {
188
+ "import": {
189
+ "types": "./dist/esm/telemetryRedaction.d.ts",
190
+ "default": "./dist/esm/telemetryRedaction.js"
191
+ },
192
+ "require": {
193
+ "types": "./dist/cjs/telemetryRedaction.d.ts",
194
+ "default": "./dist/cjs/telemetryRedaction.js"
195
+ }
186
196
  }
187
197
  },
188
198
  "dependencies": {