@skrr-ai/cli 0.1.103 → 0.1.105
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.
- package/dist/base-command.js +6 -2
- package/dist/commands/agenda/trigger/list.js +2 -0
- package/dist/commands/agents/chat.js +4 -1
- package/dist/commands/agents/operating-plan.js +5 -2
- package/dist/commands/agents/triggers/create.js +3 -1
- package/dist/commands/agents/triggers/fire.js +2 -0
- package/dist/commands/agents/triggers/replay.js +2 -0
- package/dist/commands/agents/triggers/update.js +12 -4
- package/dist/commands/commitments/action-history.js +7 -2
- package/dist/commands/commitments/cadence-decisions.js +6 -1
- package/dist/commands/commitments/cadence-requests/decide.d.ts +1 -1
- package/dist/commands/commitments/cadence-requests/decide.js +1 -1
- package/dist/commands/commitments/create.js +2 -0
- package/dist/commands/commitments/doctor.js +7 -1
- package/dist/commands/commitments/observations.js +7 -1
- package/dist/commands/commitments/operating-review.d.ts +15 -0
- package/dist/commands/commitments/operating-review.js +23 -2
- package/dist/commands/commitments/preflight.d.ts +12 -0
- package/dist/commands/commitments/preflight.js +23 -9
- package/dist/commands/commitments/reflection.js +6 -1
- package/dist/commands/commitments/show.js +3 -1
- package/dist/commands/commitments/trigger-status.js +2 -22
- package/dist/commands/commitments/wake-capabilities.d.ts +13 -0
- package/dist/commands/commitments/wake-capabilities.js +39 -3
- package/dist/commands/daemon/logs.d.ts +109 -4
- package/dist/commands/daemon/logs.js +397 -43
- package/dist/commands/followups/act.js +13 -1
- package/dist/commands/followups/checkin.js +11 -2
- package/dist/commands/followups/reschedule.js +13 -1
- package/dist/commands/proposals/discard.d.ts +1 -1
- package/dist/commands/proposals/keep.d.ts +1 -1
- package/dist/commands/proposals/list.d.ts +7 -0
- package/dist/commands/proposals/list.js +31 -5
- package/dist/commands/proposals/show.d.ts +2 -1
- package/dist/commands/proposals/show.js +31 -4
- package/dist/commands/proposals/snooze.d.ts +1 -1
- package/dist/commands/requests/create.js +8 -3
- package/dist/commands/spaces/workflows/create.js +3 -3
- package/dist/commands/tasks/complete.js +4 -0
- package/dist/commands/tasks/create.d.ts +2 -0
- package/dist/commands/tasks/create.js +17 -3
- package/dist/commands/tasks/deliverable/add.d.ts +2 -0
- package/dist/commands/tasks/deliverable/add.js +22 -7
- package/dist/commands/tasks/deliverable/list.d.ts +13 -0
- package/dist/commands/tasks/deliverable/list.js +34 -1
- package/dist/commands/tasks/economics-compare.js +7 -1
- package/dist/commands/tasks/expectations.d.ts +10 -0
- package/dist/commands/tasks/expectations.js +31 -3
- package/dist/commands/tasks/list.d.ts +9 -0
- package/dist/commands/tasks/list.js +40 -5
- package/dist/commands/tasks/report.js +1 -1
- package/dist/commands/tasks/result/show.d.ts +7 -0
- package/dist/commands/tasks/result/show.js +32 -1
- package/dist/commands/tasks/result/submit.js +1 -1
- package/dist/commands/tasks/show.d.ts +6 -0
- package/dist/commands/tasks/show.js +23 -0
- package/dist/commands/tasks/update.d.ts +3 -0
- package/dist/commands/tasks/update.js +23 -3
- package/dist/commands/triggers/audit.js +6 -2
- package/dist/commands/triggers/create.js +3 -1
- package/dist/commands/triggers/fire.d.ts +1 -0
- package/dist/commands/triggers/fire.js +16 -1
- package/dist/commands/triggers/preflight.js +6 -5
- package/dist/commands/triggers/replay.js +2 -0
- package/dist/commands/triggers/update.js +12 -4
- package/dist/commands/workspaces/stewardship.js +5 -1
- package/dist/lib/agentic-stream.d.ts +9 -0
- package/dist/lib/agentic-stream.js +38 -18
- package/dist/lib/commitment-event-wakes.d.ts +8 -0
- package/dist/lib/commitment-event-wakes.js +39 -0
- package/dist/lib/commitment-ledgers.d.ts +9 -0
- package/dist/lib/commitment-ledgers.js +126 -0
- package/dist/lib/commitments.d.ts +102 -2
- package/dist/lib/commitments.js +208 -8
- package/dist/lib/delegated-cli.js +13 -2
- package/dist/lib/followups.d.ts +6 -0
- package/dist/lib/followups.js +12 -0
- package/dist/lib/message-content.d.ts +6 -0
- package/dist/lib/message-content.js +24 -0
- package/dist/lib/operating-plan-loops.d.ts +8 -0
- package/dist/lib/operating-plan-loops.js +32 -0
- package/dist/lib/preflight-copy.d.ts +27 -0
- package/dist/lib/preflight-copy.js +89 -0
- package/dist/lib/proposal-decision-command.d.ts +1 -1
- package/dist/lib/proposal-decision-command.js +27 -3
- package/dist/lib/proposals.d.ts +17 -0
- package/dist/lib/proposals.js +41 -0
- package/dist/lib/stewardship-output.js +57 -12
- package/dist/lib/task-economics-line.d.ts +30 -0
- package/dist/lib/task-economics-line.js +49 -0
- package/dist/lib/task-economics.d.ts +15 -0
- package/dist/lib/task-economics.js +73 -8
- package/dist/lib/task-recurrence-outcome.d.ts +19 -0
- package/dist/lib/task-recurrence-outcome.js +30 -0
- package/dist/lib/triggers.d.ts +42 -0
- package/dist/lib/triggers.js +136 -2
- package/dist/lib/widgetTerminalText.d.ts +8 -0
- package/dist/lib/widgetTerminalText.js +116 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +27 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +84 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +222 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +26 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +84 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +216 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +7628 -7414
- package/docs/v1/operations.md +1 -1
- package/oclif.manifest.json +117 -73
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
* THE OTHER PATH CREDENTIALS (OSK-16174). Four more bearer credentials travel in a URL, and until they were added here
|
|
20
|
+
* only the request logger's path rewrite (`requestLogger.js`) knew them, so the same address that was scrubbed from a
|
|
21
|
+
* log line reached a trace and an error report in full: a workspace invitation (`wsi_`), a platform invitation
|
|
22
|
+
* (`pinv_`), a member's invitation link (`usr_`) and an operator's (`adm2_`, and the older `adm_`). Unlike `pvw_`, whose
|
|
23
|
+
* prefix is rare enough to match on its own, `usr_` and `adm_` begin ordinary words and identifiers (`/usr_local`,
|
|
24
|
+
* `adm_users`), so each of these is recognised by the SHAPE its minting code gives it, and a lookalike is left alone:
|
|
25
|
+
*
|
|
26
|
+
* wsi_ / pinv_ / usr_ / adm2_ `<prefix>_<32 hex>.<version>.<key id>.<43 base64url>`, from `buildCredential` in
|
|
27
|
+
* `api/models/WorkspaceInvite.js` and `PlatformInvitation.js` and `buildUserCode` and
|
|
28
|
+
* `buildAdminCode` in `InvitationCode.js`. The older member link, `usr_<32 hex>.<24
|
|
29
|
+
* base64url>`, has the same first two fields. Matched from the prefix to the end of the
|
|
30
|
+
* token's characters once the 32-hex id and its dot are there, so a credential cut off
|
|
31
|
+
* later in the log line is still redacted whole.
|
|
32
|
+
* adm_ `adm_<43 base64url>`, the older operator code (`generateCode`: 32 random bytes).
|
|
33
|
+
* Exactly 43 characters, because that is the only length it has.
|
|
34
|
+
*
|
|
35
|
+
* `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` mints credentials with the real minting
|
|
36
|
+
* code and requires every one to come out as the marker, so a change to either side fails there.
|
|
37
|
+
*
|
|
38
|
+
* WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
|
|
39
|
+
* repository writes one, and a scan that decoded every string first would cost every sink on every record. A
|
|
40
|
+
* credential cut off before its id is whole (`pinv_0123abc`) is an identifier, not yet the secret, and is left as it
|
|
41
|
+
* is. The request log's own `/[credential]` rewrite stays as it is, and is looser (any `wsi_`, `pinv_`, `usr_`,
|
|
42
|
+
* `adm_`, `adm2_` or `pvw_` path segment), because a path segment is a place a credential is known to be.
|
|
43
|
+
*
|
|
44
|
+
* COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
|
|
45
|
+
* cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
|
|
46
|
+
*
|
|
47
|
+
* NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
|
|
48
|
+
* with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
|
|
49
|
+
* record, because a record with the token still in it is exactly what this exists to prevent.
|
|
50
|
+
*/
|
|
51
|
+
/** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
|
|
52
|
+
export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
|
|
53
|
+
/**
|
|
54
|
+
* Replace every credential that travels in a URL with a marker: a Hosted Machine preview token with
|
|
55
|
+
* {@link PREVIEW_TOKEN_REDACTION}, and a workspace invitation, platform invitation, member link or operator code with
|
|
56
|
+
* its own prefix and `_[redacted]`. The rest of the string is untouched, so a trace is still readable. Anything
|
|
57
|
+
* that is not a string comes back as it came. (The name is the preview token's, which this began as; the sinks
|
|
58
|
+
* and the boundary specs import it by that name.)
|
|
59
|
+
*/
|
|
60
|
+
export declare function redactPreviewTokens(value: string): string;
|
|
61
|
+
export declare function redactPreviewTokens<T>(value: T): T;
|
|
62
|
+
export interface PreviewTokenWalkLimits {
|
|
63
|
+
/** Objects and arrays visited before the walk gives up. Default 50 000: a Sentry transaction of a thousand spans. */
|
|
64
|
+
maxNodes?: number;
|
|
65
|
+
/** Nesting followed before the walk gives up. Default 32. */
|
|
66
|
+
maxDepth?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
|
|
70
|
+
* copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
|
|
71
|
+
* the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
|
|
72
|
+
* be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
|
|
73
|
+
* not enumerable.
|
|
74
|
+
*/
|
|
75
|
+
export declare function redactPreviewTokensDeep(root: unknown, limits?: PreviewTokenWalkLimits): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
|
|
78
|
+
* are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
|
|
79
|
+
* `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
|
|
80
|
+
* An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
|
|
81
|
+
* what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
|
|
82
|
+
* Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
|
|
83
|
+
*/
|
|
84
|
+
export declare function withPreviewTokenRedaction<T extends object>(options: T): T;
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
* THE OTHER PATH CREDENTIALS (OSK-16174). Four more bearer credentials travel in a URL, and until they were added here
|
|
21
|
+
* only the request logger's path rewrite (`requestLogger.js`) knew them, so the same address that was scrubbed from a
|
|
22
|
+
* log line reached a trace and an error report in full: a workspace invitation (`wsi_`), a platform invitation
|
|
23
|
+
* (`pinv_`), a member's invitation link (`usr_`) and an operator's (`adm2_`, and the older `adm_`). Unlike `pvw_`, whose
|
|
24
|
+
* prefix is rare enough to match on its own, `usr_` and `adm_` begin ordinary words and identifiers (`/usr_local`,
|
|
25
|
+
* `adm_users`), so each of these is recognised by the SHAPE its minting code gives it, and a lookalike is left alone:
|
|
26
|
+
*
|
|
27
|
+
* wsi_ / pinv_ / usr_ / adm2_ `<prefix>_<32 hex>.<version>.<key id>.<43 base64url>`, from `buildCredential` in
|
|
28
|
+
* `api/models/WorkspaceInvite.js` and `PlatformInvitation.js` and `buildUserCode` and
|
|
29
|
+
* `buildAdminCode` in `InvitationCode.js`. The older member link, `usr_<32 hex>.<24
|
|
30
|
+
* base64url>`, has the same first two fields. Matched from the prefix to the end of the
|
|
31
|
+
* token's characters once the 32-hex id and its dot are there, so a credential cut off
|
|
32
|
+
* later in the log line is still redacted whole.
|
|
33
|
+
* adm_ `adm_<43 base64url>`, the older operator code (`generateCode`: 32 random bytes).
|
|
34
|
+
* Exactly 43 characters, because that is the only length it has.
|
|
35
|
+
*
|
|
36
|
+
* `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` mints credentials with the real minting
|
|
37
|
+
* code and requires every one to come out as the marker, so a change to either side fails there.
|
|
38
|
+
*
|
|
39
|
+
* WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
|
|
40
|
+
* repository writes one, and a scan that decoded every string first would cost every sink on every record. A
|
|
41
|
+
* credential cut off before its id is whole (`pinv_0123abc`) is an identifier, not yet the secret, and is left as it
|
|
42
|
+
* is. The request log's own `/[credential]` rewrite stays as it is, and is looser (any `wsi_`, `pinv_`, `usr_`,
|
|
43
|
+
* `adm_`, `adm2_` or `pvw_` path segment), because a path segment is a place a credential is known to be.
|
|
44
|
+
*
|
|
45
|
+
* COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
|
|
46
|
+
* cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
|
|
47
|
+
*
|
|
48
|
+
* NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
|
|
49
|
+
* with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
|
|
50
|
+
* record, because a record with the token still in it is exactly what this exists to prevent.
|
|
51
|
+
*/
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.PREVIEW_TOKEN_REDACTION = void 0;
|
|
54
|
+
exports.redactPreviewTokens = redactPreviewTokens;
|
|
55
|
+
exports.redactPreviewTokensDeep = redactPreviewTokensDeep;
|
|
56
|
+
exports.withPreviewTokenRedaction = withPreviewTokenRedaction;
|
|
57
|
+
/** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
|
|
58
|
+
exports.PREVIEW_TOKEN_REDACTION = 'pvw_[redacted]';
|
|
59
|
+
/**
|
|
60
|
+
* One character of a token: base64url, a dot, or any of those written as a percent-escape. An escape that was
|
|
61
|
+
* escaped again is `%25` followed by the original two digits (`_` is `%5F`, then `%255F`, then `%25255F`), which is
|
|
62
|
+
* what the `(?:25)*` is for. `25` is not itself a digit pair of a token character, so a run of them can only be
|
|
63
|
+
* read one way.
|
|
64
|
+
*/
|
|
65
|
+
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]))';
|
|
66
|
+
/**
|
|
67
|
+
* `pvw` then an underscore, which may be written `%5F` (or `%255F`, ...); then at least ONE token character. Not
|
|
68
|
+
* zero: the marker is `pvw_[`, and `[` is not a token character, so a string that was already redacted matches
|
|
69
|
+
* nothing and redacting twice changes nothing.
|
|
70
|
+
*/
|
|
71
|
+
const PREVIEW_TOKEN = new RegExp(`pvw(?:_|%(?:25)*5[Ff])${TOKEN_CHAR}+`, 'g');
|
|
72
|
+
/**
|
|
73
|
+
* A prefix's underscore, which (like the token's own characters) may be written as a percent-escape, once or several
|
|
74
|
+
* times; and the dot that ends an id.
|
|
75
|
+
*/
|
|
76
|
+
const UNDERSCORE = '(?:_|%(?:25)*5[Ff])';
|
|
77
|
+
const DOT = '(?:\\.|%(?:25)*2[Ee])';
|
|
78
|
+
/**
|
|
79
|
+
* `wsi`, `pinv`, `usr` or `adm2`, an underscore, and then a lookahead that insists on what the minting code writes
|
|
80
|
+
* next: 32 lowercase hex characters and the dot that ends them. Everything the credential is made of follows, and the
|
|
81
|
+
* prefix is captured so the marker can say which kind it was. `usr_<40 hex>`, `usr_00000001` and `/usr_profile` have no
|
|
82
|
+
* dot after exactly 32 hex characters, so they match nothing.
|
|
83
|
+
*/
|
|
84
|
+
const SIGNED_CREDENTIAL = new RegExp(`(wsi|pinv|usr|adm2)${UNDERSCORE}(?=[0-9a-f]{32}${DOT})${TOKEN_CHAR}+`, 'g');
|
|
85
|
+
/**
|
|
86
|
+
* The older operator code: `adm_` and exactly 43 base64url characters (32 random bytes), not followed by another one.
|
|
87
|
+
* A shorter or longer run of letters is a name, not this credential. The characters are the plain alphabet and not
|
|
88
|
+
* `TOKEN_CHAR`, whose dot would swallow the full stop at the end of a sentence.
|
|
89
|
+
*/
|
|
90
|
+
const LEGACY_OPERATOR_CODE = new RegExp(`(adm)${UNDERSCORE}(?=[A-Za-z0-9_-]{43}(?![A-Za-z0-9_-]))[A-Za-z0-9_-]{43}`, 'g');
|
|
91
|
+
/** Cheap test before any of the expressions: a string with none of these cannot hold a credential. */
|
|
92
|
+
const CREDENTIAL_HINTS = ['pvw', 'wsi', 'pinv', 'usr', 'adm'];
|
|
93
|
+
const credentialMarker = (_match, prefix) => `${prefix}_[redacted]`;
|
|
94
|
+
function redactPreviewTokens(value) {
|
|
95
|
+
if (typeof value !== 'string' || !CREDENTIAL_HINTS.some((hint) => value.indexOf(hint) !== -1)) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
return value
|
|
99
|
+
.replace(PREVIEW_TOKEN, exports.PREVIEW_TOKEN_REDACTION)
|
|
100
|
+
.replace(SIGNED_CREDENTIAL, credentialMarker)
|
|
101
|
+
.replace(LEGACY_OPERATOR_CODE, credentialMarker);
|
|
102
|
+
}
|
|
103
|
+
const DEFAULT_MAX_NODES = 50_000;
|
|
104
|
+
const DEFAULT_MAX_DEPTH = 32;
|
|
105
|
+
/**
|
|
106
|
+
* Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
|
|
107
|
+
* copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
|
|
108
|
+
* the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
|
|
109
|
+
* be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
|
|
110
|
+
* not enumerable.
|
|
111
|
+
*/
|
|
112
|
+
function redactPreviewTokensDeep(root, limits = {}) {
|
|
113
|
+
if (root === null || typeof root !== 'object') {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const maxNodes = limits.maxNodes ?? DEFAULT_MAX_NODES;
|
|
117
|
+
const maxDepth = limits.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
118
|
+
try {
|
|
119
|
+
const seen = new WeakSet();
|
|
120
|
+
const stack = [[root, 0]];
|
|
121
|
+
let visited = 0;
|
|
122
|
+
let complete = true;
|
|
123
|
+
const rewrite = (holder, key, current) => {
|
|
124
|
+
const next = redactPreviewTokens(current);
|
|
125
|
+
if (next === current) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
holder[key] = next;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// a frozen object throws in strict code
|
|
133
|
+
}
|
|
134
|
+
if (holder[key] !== next) {
|
|
135
|
+
// and silently keeps the old value in sloppy code
|
|
136
|
+
complete = false;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
while (stack.length > 0) {
|
|
140
|
+
const [node, depth] = stack.pop();
|
|
141
|
+
if (seen.has(node) || ArrayBuffer.isView(node)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
seen.add(node);
|
|
145
|
+
visited += 1;
|
|
146
|
+
if (visited > maxNodes) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const holder = node;
|
|
150
|
+
if (node instanceof Error) {
|
|
151
|
+
for (const key of ['message', 'stack']) {
|
|
152
|
+
if (typeof holder[key] === 'string') {
|
|
153
|
+
rewrite(holder, key, holder[key]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const key of Object.keys(node)) {
|
|
158
|
+
let child;
|
|
159
|
+
try {
|
|
160
|
+
child = holder[key];
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
complete = false;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (typeof child === 'string') {
|
|
167
|
+
rewrite(holder, key, child);
|
|
168
|
+
}
|
|
169
|
+
else if (child !== null && typeof child === 'object') {
|
|
170
|
+
if (depth + 1 > maxDepth) {
|
|
171
|
+
complete = false;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
stack.push([child, depth + 1]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return complete;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function isThenable(value) {
|
|
186
|
+
return (value !== null &&
|
|
187
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
188
|
+
typeof value.then === 'function');
|
|
189
|
+
}
|
|
190
|
+
/** The record with its tokens redacted, or `null` (dropped) when that could not be done. */
|
|
191
|
+
function scrubRecord(record) {
|
|
192
|
+
if (record === null || record === undefined) {
|
|
193
|
+
return record;
|
|
194
|
+
}
|
|
195
|
+
if (typeof record !== 'object') {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return redactPreviewTokensDeep(record) ? record : null;
|
|
199
|
+
}
|
|
200
|
+
function guard(existing) {
|
|
201
|
+
return (value, hint) => {
|
|
202
|
+
const result = existing ? existing(value, hint) : value;
|
|
203
|
+
return isThenable(result) ? result.then(scrubRecord) : scrubRecord(result);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
|
|
208
|
+
* are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
|
|
209
|
+
* `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
|
|
210
|
+
* An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
|
|
211
|
+
* what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
|
|
212
|
+
* Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
|
|
213
|
+
*/
|
|
214
|
+
function withPreviewTokenRedaction(options) {
|
|
215
|
+
const hooks = options;
|
|
216
|
+
return {
|
|
217
|
+
...options,
|
|
218
|
+
beforeSend: guard(hooks.beforeSend),
|
|
219
|
+
beforeSendTransaction: guard(hooks.beforeSendTransaction),
|
|
220
|
+
beforeBreadcrumb: guard(hooks.beforeBreadcrumb),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -133,3 +133,36 @@ export declare function parseCliHandoffDpopProofResponse(value: unknown): CliHan
|
|
|
133
133
|
export declare const DELEGATED_AGENT_CLI_ID_PREFIX = "oversky-daemon-agent-cli-";
|
|
134
134
|
export declare const WORKLOAD_CLI_ID_PREFIX = "oversky-daemon-workload-cli-";
|
|
135
135
|
export declare function classifyCliFamilyId(id: unknown): 'agent' | 'workload' | 'person';
|
|
136
|
+
/**
|
|
137
|
+
* Who keeps a daemon running, which decides how it is signed in again.
|
|
138
|
+
* - `desktop`: the Skrr desktop app hosts it (Electron-as-Node under the
|
|
139
|
+
* app's LaunchAgent). The app mints the daemon's credential itself at
|
|
140
|
+
* `POST /api/daemons/token`, with a device proof, on every start of its
|
|
141
|
+
* daemon client (OSK-15990), so reopening the app IS the sign-in. Neither
|
|
142
|
+
* login command repairs it: `skrr daemon login` refuses an Electron-hosted
|
|
143
|
+
* daemon (its program is the app, not a `skrrd`), and a `skrrd` on PATH is
|
|
144
|
+
* usually not installed, or is a different build from the one running.
|
|
145
|
+
* - `service`: `skrrd install` (launchd/systemd/schtasks) or a hand start;
|
|
146
|
+
* `skrrd login` for that profile, server and working directory is the fix.
|
|
147
|
+
*/
|
|
148
|
+
export type DaemonSupervisor = 'desktop' | 'service';
|
|
149
|
+
/**
|
|
150
|
+
* The ONE sentence for "only a new daemon sign-in repairs this hand-off
|
|
151
|
+
* refusal" (OSK-16216). The daemon fills it in with what it knows about
|
|
152
|
+
* itself and sends it as the loopback 503's `remedy`; the CLI uses it, with
|
|
153
|
+
* nothing filled in, when an older daemon sent none. Before this the two were
|
|
154
|
+
* written separately and disagreed — the CLI said `skrr daemon login`, the
|
|
155
|
+
* daemon said `skrrd --profile … login` — and neither was right for a daemon
|
|
156
|
+
* the desktop app runs.
|
|
157
|
+
*
|
|
158
|
+
* Returned without a leading capital or trailing period so callers can put it
|
|
159
|
+
* after "Fix: " or "— ".
|
|
160
|
+
*/
|
|
161
|
+
export declare function daemonSignInRemedy(opts?: {
|
|
162
|
+
/** How the daemon is run; absent when the caller cannot tell. */
|
|
163
|
+
supervisor?: DaemonSupervisor;
|
|
164
|
+
/** The exact `skrrd … login` for this profile, when the daemon knows it. */
|
|
165
|
+
loginCommand?: string;
|
|
166
|
+
/** The refusal was DEVICE_PROOF_PIN_MISMATCH: say that a sign-in re-pins. */
|
|
167
|
+
repinsDeviceKey?: boolean;
|
|
168
|
+
}): string;
|
|
@@ -145,3 +145,29 @@ export function classifyCliFamilyId(id) {
|
|
|
145
145
|
return 'workload';
|
|
146
146
|
return 'person';
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* The ONE sentence for "only a new daemon sign-in repairs this hand-off
|
|
150
|
+
* refusal" (OSK-16216). The daemon fills it in with what it knows about
|
|
151
|
+
* itself and sends it as the loopback 503's `remedy`; the CLI uses it, with
|
|
152
|
+
* nothing filled in, when an older daemon sent none. Before this the two were
|
|
153
|
+
* written separately and disagreed — the CLI said `skrr daemon login`, the
|
|
154
|
+
* daemon said `skrrd --profile … login` — and neither was right for a daemon
|
|
155
|
+
* the desktop app runs.
|
|
156
|
+
*
|
|
157
|
+
* Returned without a leading capital or trailing period so callers can put it
|
|
158
|
+
* after "Fix: " or "— ".
|
|
159
|
+
*/
|
|
160
|
+
export function daemonSignInRemedy(opts = {}) {
|
|
161
|
+
const repin = opts.repinsDeviceKey === true;
|
|
162
|
+
if (opts.supervisor === 'desktop') {
|
|
163
|
+
return ('quit and reopen the Skrr desktop app on the machine running the daemon (install its update ' +
|
|
164
|
+
`first if one is offered): the app signs its daemon in again on start${repin ? ' and re-pins the device key' : ''}`);
|
|
165
|
+
}
|
|
166
|
+
if (opts.supervisor === 'service') {
|
|
167
|
+
const login = opts.loginCommand ? `\`${opts.loginCommand}\`` : '`skrr daemon login`';
|
|
168
|
+
return `run ${login}${repin ? ' — an interactive login re-pins the device key' : ''}`;
|
|
169
|
+
}
|
|
170
|
+
return ('sign the daemon in again on the machine running it: if the Skrr desktop app runs it, quit ' +
|
|
171
|
+
'and reopen the app; otherwise run `skrr daemon login`' +
|
|
172
|
+
(repin ? ' (either one re-pins the device key)' : ''));
|
|
173
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
* THE OTHER PATH CREDENTIALS (OSK-16174). Four more bearer credentials travel in a URL, and until they were added here
|
|
20
|
+
* only the request logger's path rewrite (`requestLogger.js`) knew them, so the same address that was scrubbed from a
|
|
21
|
+
* log line reached a trace and an error report in full: a workspace invitation (`wsi_`), a platform invitation
|
|
22
|
+
* (`pinv_`), a member's invitation link (`usr_`) and an operator's (`adm2_`, and the older `adm_`). Unlike `pvw_`, whose
|
|
23
|
+
* prefix is rare enough to match on its own, `usr_` and `adm_` begin ordinary words and identifiers (`/usr_local`,
|
|
24
|
+
* `adm_users`), so each of these is recognised by the SHAPE its minting code gives it, and a lookalike is left alone:
|
|
25
|
+
*
|
|
26
|
+
* wsi_ / pinv_ / usr_ / adm2_ `<prefix>_<32 hex>.<version>.<key id>.<43 base64url>`, from `buildCredential` in
|
|
27
|
+
* `api/models/WorkspaceInvite.js` and `PlatformInvitation.js` and `buildUserCode` and
|
|
28
|
+
* `buildAdminCode` in `InvitationCode.js`. The older member link, `usr_<32 hex>.<24
|
|
29
|
+
* base64url>`, has the same first two fields. Matched from the prefix to the end of the
|
|
30
|
+
* token's characters once the 32-hex id and its dot are there, so a credential cut off
|
|
31
|
+
* later in the log line is still redacted whole.
|
|
32
|
+
* adm_ `adm_<43 base64url>`, the older operator code (`generateCode`: 32 random bytes).
|
|
33
|
+
* Exactly 43 characters, because that is the only length it has.
|
|
34
|
+
*
|
|
35
|
+
* `api/server/services/HostedPreview/__tests__/telemetryRedaction.spec.js` mints credentials with the real minting
|
|
36
|
+
* code and requires every one to come out as the marker, so a change to either side fails there.
|
|
37
|
+
*
|
|
38
|
+
* WHAT IT DOES NOT. A prefix that is itself percent-encoded (`%70vw_`) is not recognised: nothing in this
|
|
39
|
+
* repository writes one, and a scan that decoded every string first would cost every sink on every record. A
|
|
40
|
+
* credential cut off before its id is whole (`pinv_0123abc`) is an identifier, not yet the secret, and is left as it
|
|
41
|
+
* is. The request log's own `/[credential]` rewrite stays as it is, and is looser (any `wsi_`, `pinv_`, `usr_`,
|
|
42
|
+
* `adm_`, `adm2_` or `pvw_` path segment), because a path segment is a place a credential is known to be.
|
|
43
|
+
*
|
|
44
|
+
* COST. Linear in the input. The only nested repetition is `%(?:25)*XX`, and `XX` is never `25`, so the engine
|
|
45
|
+
* cannot read one run of `25`s two ways; the tests feed it a megabyte of the worst shapes.
|
|
46
|
+
*
|
|
47
|
+
* NEVER THROWS. These run inside telemetry hooks, and a hook that throws takes a request or an error report
|
|
48
|
+
* with it. Whatever cannot be scrubbed is reported to the caller as such (`false`), and the caller drops the
|
|
49
|
+
* record, because a record with the token still in it is exactly what this exists to prevent.
|
|
50
|
+
*/
|
|
51
|
+
/** What replaces a token. Fixed, so a trace can say a preview was involved but not which one. */
|
|
52
|
+
export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
|
|
53
|
+
/**
|
|
54
|
+
* Replace every credential that travels in a URL with a marker: a Hosted Machine preview token with
|
|
55
|
+
* {@link PREVIEW_TOKEN_REDACTION}, and a workspace invitation, platform invitation, member link or operator code with
|
|
56
|
+
* its own prefix and `_[redacted]`. The rest of the string is untouched, so a trace is still readable. Anything
|
|
57
|
+
* that is not a string comes back as it came. (The name is the preview token's, which this began as; the sinks
|
|
58
|
+
* and the boundary specs import it by that name.)
|
|
59
|
+
*/
|
|
60
|
+
export declare function redactPreviewTokens(value: string): string;
|
|
61
|
+
export declare function redactPreviewTokens<T>(value: T): T;
|
|
62
|
+
export interface PreviewTokenWalkLimits {
|
|
63
|
+
/** Objects and arrays visited before the walk gives up. Default 50 000: a Sentry transaction of a thousand spans. */
|
|
64
|
+
maxNodes?: number;
|
|
65
|
+
/** Nesting followed before the walk gives up. Default 32. */
|
|
66
|
+
maxDepth?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Redact every string inside an object graph, IN PLACE (an error report is rewritten far more cheaply than it is
|
|
70
|
+
* copied). Returns `true` when the whole graph was scrubbed and `false` when some of it could not be: past
|
|
71
|
+
* the bounds, frozen, behind a getter that throws. A `false` means the value may still hold a token and must not
|
|
72
|
+
* be sent. Binary payloads are skipped, not walked, and Error messages and stacks are scrubbed though they are
|
|
73
|
+
* not enumerable.
|
|
74
|
+
*/
|
|
75
|
+
export declare function redactPreviewTokensDeep(root: unknown, limits?: PreviewTokenWalkLimits): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Sentry `init` options with a redacting `beforeSend`, `beforeSendTransaction` and `beforeBreadcrumb`. Those three
|
|
78
|
+
* are where an event's `request.url`, `request.query_string`, `transaction`, spans, breadcrumbs, `extra` and
|
|
79
|
+
* `contexts` pass last, so redacting the whole record there needs no list of fields and covers one added later.
|
|
80
|
+
* An existing hook runs FIRST and keeps its say (it can still drop the record, change it, or be asynchronous);
|
|
81
|
+
* what it returns is scrubbed after it, so nothing it adds escapes. The options object passed in is not modified.
|
|
82
|
+
* Every `Sentry.init` in this repository goes through this, and an architecture spec fails if one is added without.
|
|
83
|
+
*/
|
|
84
|
+
export declare function withPreviewTokenRedaction<T extends object>(options: T): T;
|