@hyperdrive.bot/paseo-protocol 0.3.52 → 0.3.54

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/messages.js CHANGED
@@ -835,6 +835,26 @@ export const SessionDigestLinkSchema = z.object({
835
835
  url: z.string(),
836
836
  label: z.string().optional(),
837
837
  });
838
+ /**
839
+ * A credential row on the session context rail.
840
+ *
841
+ * `value` is ALWAYS safe to render: the daemon runs every incoming value
842
+ * through `normalizeSecretForStorage` (see session-secrets.ts), so a live-shaped
843
+ * token is masked before it is ever persisted, pushed, or cached. That is why
844
+ * the app can offer a reveal button at all -- it uncovers something that was
845
+ * already harmless, not something the view layer was trusted to hide.
846
+ */
847
+ export const SessionSecretSchema = z.object({
848
+ /** What it is, e.g. "GITLAB_TOKEN", "staging db user". Unique per digest. */
849
+ label: z.string().min(1).max(80),
850
+ /** Safe-to-render value: a placeholder verbatim, or a mask like "••••7f3a". */
851
+ value: z.string().max(200),
852
+ /** True when `value` is a mask rather than the real string. */
853
+ masked: z.boolean(),
854
+ /** Provenance, e.g. "env:GITLAB_TOKEN" or ".env.local". Never a value. */
855
+ source: z.string().max(200).optional(),
856
+ discoveredAt: z.string().optional(),
857
+ });
838
858
  /**
839
859
  * Session digest — structured, machine-readable per-session metadata so an
840
860
  * orchestrator can triage a fleet without replaying transcripts. See
@@ -848,6 +868,13 @@ export const SessionDigestSchema = z.object({
848
868
  nextStep: z.string().optional(),
849
869
  blockers: z.array(z.string()).optional(),
850
870
  links: z.array(SessionDigestLinkSchema).optional(),
871
+ /**
872
+ * Non-live credentials and connection hints a session needs at hand:
873
+ * which token it authenticates with, which test user, which host. Written
874
+ * by the context hook, masked at the daemon boundary. See
875
+ * docs/session-context-rail.md.
876
+ */
877
+ secrets: z.array(SessionSecretSchema).optional(),
851
878
  keyFiles: z.array(z.string()).optional(),
852
879
  summary: z.string().optional(),
853
880
  accomplishments: z.array(z.string()).optional(),
@@ -1052,6 +1079,94 @@ export const BackgroundTaskOutputResponseSchema = z.object({
1052
1079
  requestId: z.string(),
1053
1080
  }),
1054
1081
  });
1082
+ /**
1083
+ * What the daemon could find out about one external reference (a GitLab
1084
+ * pipeline, a merge request, a GitHub PR, a Jira issue).
1085
+ *
1086
+ * Every field is nullable on purpose. Enrichment needs a provider credential
1087
+ * the daemon may simply not have, and the honest answer to "what is this
1088
+ * pipeline doing" is often "I cannot see it" -- which is what
1089
+ * `unavailableReason` carries, so the card can say so instead of spinning.
1090
+ */
1091
+ export const ReferenceEnrichmentSchema = z.object({
1092
+ url: z.string(),
1093
+ kind: z.string(),
1094
+ /** Provider status verbatim: "running", "success", "In Review", "merged". */
1095
+ status: z.string().nullable(),
1096
+ /** A normalized bucket the UI colors on, so it never parses `status`. */
1097
+ state: z.enum(["running", "success", "failed", "blocked", "neutral"]),
1098
+ title: z.string().nullable(),
1099
+ /** One supporting line: author, branch, assignee. */
1100
+ detail: z.string().nullable(),
1101
+ /** Countable progress where the provider has it, e.g. "12/14 jobs". */
1102
+ progress: z.string().nullable(),
1103
+ updatedAt: z.string().nullable(),
1104
+ /** Set when nothing could be fetched. Null on success. */
1105
+ unavailableReason: z.string().nullable(),
1106
+ /** True while the underlying resource is still moving and worth re-polling. */
1107
+ live: z.boolean(),
1108
+ });
1109
+ /**
1110
+ * Ask the daemon to enrich reference URLs. On demand (like
1111
+ * `list_background_tasks_request`), never on the snapshot hot path: the app
1112
+ * polls this only while a context rail carrying live references is on screen.
1113
+ *
1114
+ * Credentials stay on the daemon. The app sends URLs and receives status.
1115
+ */
1116
+ /**
1117
+ * Record session context (external links + non-live credentials) onto an
1118
+ * agent's digest from OUTSIDE the agent's own MCP session.
1119
+ *
1120
+ * This is the channel the context hook uses. `set_session_digest` is
1121
+ * agent-scoped -- only the agent itself can call it -- but a Claude Code hook is
1122
+ * a shell process beside the agent, not the agent, so it needs a door of its
1123
+ * own. Merge semantics: links union by URL, secrets by label (later wins), and
1124
+ * every secret is masked by the daemon before it is stored.
1125
+ */
1126
+ export const SetAgentContextRequestMessageSchema = z.object({
1127
+ type: z.literal("set_agent_context_request"),
1128
+ agentId: z.string(),
1129
+ links: z
1130
+ .array(z.object({
1131
+ kind: z.enum(["pr", "mr", "issue", "deploy", "preview", "doc", "other"]),
1132
+ url: z.string().min(1).max(500),
1133
+ label: z.string().max(80).optional(),
1134
+ }))
1135
+ .max(20)
1136
+ .optional(),
1137
+ secrets: z
1138
+ .array(z.object({
1139
+ label: z.string().min(1).max(80),
1140
+ value: z.string().max(200),
1141
+ source: z.string().max(200).optional(),
1142
+ }))
1143
+ .max(20)
1144
+ .optional(),
1145
+ requestId: z.string(),
1146
+ });
1147
+ export const SetAgentContextResponseSchema = z.object({
1148
+ type: z.literal("set_agent_context_response"),
1149
+ payload: z.object({
1150
+ agentId: z.string(),
1151
+ digest: SessionDigestSchema.nullable(),
1152
+ /** How many secrets the daemon had to mask, so the hook can report it. */
1153
+ maskedCount: z.number().int().nonnegative(),
1154
+ error: z.string().optional(),
1155
+ requestId: z.string(),
1156
+ }),
1157
+ });
1158
+ export const EnrichReferencesRequestMessageSchema = z.object({
1159
+ type: z.literal("enrich_references_request"),
1160
+ urls: z.array(z.string().min(1).max(500)).max(20),
1161
+ requestId: z.string(),
1162
+ });
1163
+ export const EnrichReferencesResponseSchema = z.object({
1164
+ type: z.literal("enrich_references_response"),
1165
+ payload: z.object({
1166
+ enrichments: z.array(ReferenceEnrichmentSchema),
1167
+ requestId: z.string(),
1168
+ }),
1169
+ });
1055
1170
  export const DismissBackgroundTaskRequestMessageSchema = z.object({
1056
1171
  type: z.literal("dismiss_background_task_request"),
1057
1172
  agentId: z.string(),
@@ -2604,6 +2719,8 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
2604
2719
  DismissBackgroundTaskRequestMessageSchema,
2605
2720
  ListBackgroundTasksRequestMessageSchema,
2606
2721
  BackgroundTaskOutputRequestMessageSchema,
2722
+ EnrichReferencesRequestMessageSchema,
2723
+ SetAgentContextRequestMessageSchema,
2607
2724
  CloseItemsRequestMessageSchema,
2608
2725
  UpdateAgentRequestMessageSchema,
2609
2726
  ProjectRenameRequestSchema,
@@ -4796,6 +4913,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
4796
4913
  BackgroundTaskDismissedMessageSchema,
4797
4914
  ListBackgroundTasksResponseSchema,
4798
4915
  BackgroundTaskOutputResponseSchema,
4916
+ EnrichReferencesResponseSchema,
4917
+ SetAgentContextResponseSchema,
4799
4918
  CloseItemsResponseSchema,
4800
4919
  CheckoutStatusResponseSchema,
4801
4920
  CheckoutStatusUpdateSchema,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Session references -- turning a bare URL sitting in a session's digest into
3
+ * something the UI can render as a card instead of a link.
4
+ *
5
+ * The split matters. Classification is **pure and local**: given a URL string,
6
+ * decide what it is and pull the ids out of the path. Enrichment (is that
7
+ * pipeline green? what is that ticket called?) needs credentials and a network
8
+ * hop, so it lives on the daemon (`enrich_reference_request`) and never here.
9
+ *
10
+ * That split is the whole reason the app can show a useful reference card for a
11
+ * GitLab pipeline the instant the URL lands, and fill in "running / 12 of 14
12
+ * jobs" a moment later, without ever holding a provider token itself.
13
+ */
14
+ export type SessionReferenceKind = "gitlab-pipeline" | "gitlab-merge-request" | "github-pull-request" | "jira-issue" | "generic";
15
+ export interface SessionReference {
16
+ kind: SessionReferenceKind;
17
+ /** The URL exactly as it was written, so click-through is never lossy. */
18
+ url: string;
19
+ host: string;
20
+ /**
21
+ * Provider-scoped project path: `dev_squad/repo/tooling/paseo` on GitLab,
22
+ * `owner/repo` on GitHub, the site host on Jira. Null when the shape did not
23
+ * carry one.
24
+ */
25
+ project: string | null;
26
+ /** Pipeline id, MR iid, PR number, or issue key. Null for `generic`. */
27
+ id: string | null;
28
+ /** Compact label for a chip: `!123`, `#45`, `PENG-12`, `pipeline 2009`. */
29
+ label: string;
30
+ }
31
+ /**
32
+ * Classify one URL. Returns `null` only when the string is not a usable web
33
+ * URL at all -- an unrecognised host still comes back as `generic`, because a
34
+ * link the UI cannot enrich is still a link the UI should show.
35
+ */
36
+ export declare function classifySessionReference(rawUrl: string): SessionReference | null;
37
+ /** Classify many, dropping unusable strings and de-duplicating by URL. */
38
+ export declare function classifySessionReferences(rawUrls: readonly string[]): SessionReference[];
39
+ /** True when the daemon has a fetcher that can say something about this kind. */
40
+ export declare function isEnrichableReference(reference: SessionReference): boolean;
41
+ //# sourceMappingURL=session-references.d.ts.map
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Session references -- turning a bare URL sitting in a session's digest into
3
+ * something the UI can render as a card instead of a link.
4
+ *
5
+ * The split matters. Classification is **pure and local**: given a URL string,
6
+ * decide what it is and pull the ids out of the path. Enrichment (is that
7
+ * pipeline green? what is that ticket called?) needs credentials and a network
8
+ * hop, so it lives on the daemon (`enrich_reference_request`) and never here.
9
+ *
10
+ * That split is the whole reason the app can show a useful reference card for a
11
+ * GitLab pipeline the instant the URL lands, and fill in "running / 12 of 14
12
+ * jobs" a moment later, without ever holding a provider token itself.
13
+ */
14
+ /**
15
+ * GitLab nests groups arbitrarily deep, so the project path is everything
16
+ * before the `/-/` separator. Anything after it is the resource.
17
+ */
18
+ const GITLAB_RESOURCE_SEPARATOR = "/-/";
19
+ const JIRA_ISSUE_KEY = /^[A-Z][A-Z0-9]+-\d+$/;
20
+ function safeParse(rawUrl) {
21
+ const trimmed = rawUrl.trim();
22
+ if (trimmed.length === 0) {
23
+ return null;
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = new URL(trimmed);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ // Only web references get cards. A `file://` or `mailto:` is still a link,
33
+ // just not something a provider can be asked about.
34
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
35
+ return null;
36
+ }
37
+ return parsed;
38
+ }
39
+ function segments(pathname) {
40
+ return pathname.split("/").filter((segment) => segment.length > 0);
41
+ }
42
+ function generic(url, parsed) {
43
+ return {
44
+ kind: "generic",
45
+ url,
46
+ host: parsed.host,
47
+ project: null,
48
+ id: null,
49
+ label: parsed.host,
50
+ };
51
+ }
52
+ function classifyGitLab(url, parsed) {
53
+ const separatorIndex = parsed.pathname.indexOf(GITLAB_RESOURCE_SEPARATOR);
54
+ if (separatorIndex === -1) {
55
+ return null;
56
+ }
57
+ const project = segments(parsed.pathname.slice(0, separatorIndex)).join("/");
58
+ const resource = segments(parsed.pathname.slice(separatorIndex + GITLAB_RESOURCE_SEPARATOR.length));
59
+ if (project.length === 0 || resource.length < 2) {
60
+ return null;
61
+ }
62
+ const [kind, id] = resource;
63
+ if (kind === "pipelines" && /^\d+$/.test(id)) {
64
+ return {
65
+ kind: "gitlab-pipeline",
66
+ url,
67
+ host: parsed.host,
68
+ project,
69
+ id,
70
+ label: `pipeline ${id}`,
71
+ };
72
+ }
73
+ if (kind === "merge_requests" && /^\d+$/.test(id)) {
74
+ return {
75
+ kind: "gitlab-merge-request",
76
+ url,
77
+ host: parsed.host,
78
+ project,
79
+ id,
80
+ label: `!${id}`,
81
+ };
82
+ }
83
+ return null;
84
+ }
85
+ function classifyGitHub(url, parsed) {
86
+ if (parsed.host !== "github.com" && !parsed.host.endsWith(".github.com")) {
87
+ return null;
88
+ }
89
+ const parts = segments(parsed.pathname);
90
+ if (parts.length < 4) {
91
+ return null;
92
+ }
93
+ const [owner, repo, resource, id] = parts;
94
+ if (resource !== "pull" || !/^\d+$/.test(id)) {
95
+ return null;
96
+ }
97
+ return {
98
+ kind: "github-pull-request",
99
+ url,
100
+ host: parsed.host,
101
+ project: `${owner}/${repo}`,
102
+ id,
103
+ label: `#${id}`,
104
+ };
105
+ }
106
+ /**
107
+ * Jira writes the same issue two ways: the canonical `/browse/KEY-1`, and a
108
+ * board URL that carries the issue in `?selectedIssue=KEY-1`. Both are what a
109
+ * human actually pastes, so both resolve to the same card.
110
+ */
111
+ function classifyJira(url, parsed) {
112
+ const selected = parsed.searchParams.get("selectedIssue");
113
+ if (selected && JIRA_ISSUE_KEY.test(selected)) {
114
+ return {
115
+ kind: "jira-issue",
116
+ url,
117
+ host: parsed.host,
118
+ project: parsed.host,
119
+ id: selected,
120
+ label: selected,
121
+ };
122
+ }
123
+ const parts = segments(parsed.pathname);
124
+ const browseIndex = parts.indexOf("browse");
125
+ if (browseIndex === -1) {
126
+ return null;
127
+ }
128
+ const key = parts[browseIndex + 1];
129
+ if (!key || !JIRA_ISSUE_KEY.test(key)) {
130
+ return null;
131
+ }
132
+ return {
133
+ kind: "jira-issue",
134
+ url,
135
+ host: parsed.host,
136
+ project: parsed.host,
137
+ id: key,
138
+ label: key,
139
+ };
140
+ }
141
+ /**
142
+ * Classify one URL. Returns `null` only when the string is not a usable web
143
+ * URL at all -- an unrecognised host still comes back as `generic`, because a
144
+ * link the UI cannot enrich is still a link the UI should show.
145
+ */
146
+ export function classifySessionReference(rawUrl) {
147
+ const parsed = safeParse(rawUrl);
148
+ if (!parsed) {
149
+ return null;
150
+ }
151
+ const url = rawUrl.trim();
152
+ return (classifyGitLab(url, parsed) ??
153
+ classifyGitHub(url, parsed) ??
154
+ classifyJira(url, parsed) ??
155
+ generic(url, parsed));
156
+ }
157
+ /** Classify many, dropping unusable strings and de-duplicating by URL. */
158
+ export function classifySessionReferences(rawUrls) {
159
+ const seen = new Set();
160
+ const out = [];
161
+ for (const rawUrl of rawUrls) {
162
+ const reference = classifySessionReference(rawUrl);
163
+ if (!reference || seen.has(reference.url)) {
164
+ continue;
165
+ }
166
+ seen.add(reference.url);
167
+ out.push(reference);
168
+ }
169
+ return out;
170
+ }
171
+ /** True when the daemon has a fetcher that can say something about this kind. */
172
+ export function isEnrichableReference(reference) {
173
+ return reference.kind !== "generic";
174
+ }
175
+ //# sourceMappingURL=session-references.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Session secrets -- the credential rows a session's context rail shows.
3
+ *
4
+ * The point of this module is one rule, and the rule is the reason it is in the
5
+ * protocol package rather than the app: **a secret is masked on the way IN, at
6
+ * the daemon boundary, never on the way out at the UI.**
7
+ *
8
+ * A masked-at-render design puts the real string in the snapshot, the push
9
+ * payload, the query cache and every log line that ever prints one, and asks
10
+ * the view layer to be careful forever. Masking at write means the dangerous
11
+ * value never enters paseo at all, so the reveal button can only ever uncover
12
+ * something that was already safe to hold.
13
+ */
14
+ /** True when `value` looks like something that would still work if pasted. */
15
+ export declare function looksLikeLiveSecret(value: string): boolean;
16
+ /**
17
+ * Replace a value with something safe to store and render. Keeps a short tail
18
+ * so "is this the same token I put in the CI variable?" stays answerable, which
19
+ * is the only reason to show a credential row at all.
20
+ */
21
+ export declare function maskSecretValue(value: string): string;
22
+ export interface NormalizedSecret {
23
+ value: string;
24
+ masked: boolean;
25
+ }
26
+ /**
27
+ * The daemon-boundary call. Give it whatever the hook found; get back what is
28
+ * safe to persist, plus whether it had to be masked.
29
+ *
30
+ * `declaredMasked` lets a caller that already masked upstream say so, and is
31
+ * ignored when the value still looks live -- a caller cannot assert its way
32
+ * past the check.
33
+ */
34
+ export declare function normalizeSecretForStorage(value: string, declaredMasked?: boolean): NormalizedSecret;
35
+ //# sourceMappingURL=session-secrets.d.ts.map
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Session secrets -- the credential rows a session's context rail shows.
3
+ *
4
+ * The point of this module is one rule, and the rule is the reason it is in the
5
+ * protocol package rather than the app: **a secret is masked on the way IN, at
6
+ * the daemon boundary, never on the way out at the UI.**
7
+ *
8
+ * A masked-at-render design puts the real string in the snapshot, the push
9
+ * payload, the query cache and every log line that ever prints one, and asks
10
+ * the view layer to be careful forever. Masking at write means the dangerous
11
+ * value never enters paseo at all, so the reveal button can only ever uncover
12
+ * something that was already safe to hold.
13
+ */
14
+ /**
15
+ * Shapes that are live credentials by construction. These are matched on the
16
+ * VALUE, so the caller does not get to opt out by naming the field innocently.
17
+ *
18
+ * Deliberately conservative: it is fine to mask a harmless string, and not fine
19
+ * to store a live one. When in doubt, this list masks.
20
+ */
21
+ const LIVE_SECRET_PATTERNS = [
22
+ /\bglpat-[A-Za-z0-9_-]{12,}/, // GitLab personal access token
23
+ /\bglrt-[A-Za-z0-9_-]{12,}/, // GitLab runner token
24
+ /\bgh[pousr]_[A-Za-z0-9]{20,}/, // GitHub token family
25
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/,
26
+ /\bsk-[A-Za-z0-9_-]{20,}/, // OpenAI-style
27
+ /\bsk-ant-[A-Za-z0-9_-]{20,}/, // Anthropic
28
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/, // Slack
29
+ /\bAKIA[0-9A-Z]{16}\b/, // AWS access key id
30
+ /\bASIA[0-9A-Z]{16}\b/, // AWS temporary access key id
31
+ /\bAIza[0-9A-Za-z_-]{30,}/, // Google API key
32
+ /\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, // JWT
33
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
34
+ ];
35
+ /**
36
+ * Backstop for providers whose prefix nobody has added yet. New vendors mint new
37
+ * prefixes constantly, so length alone used to be the rule here -- and that was
38
+ * wrong in a way only running it revealed: a preview hostname
39
+ * (`paseo-app-preview-feat-session-context-rail.workers.dev`) was masked, which
40
+ * is the exact opposite of useful, since a connection hint is half the reason
41
+ * this section exists.
42
+ *
43
+ * Entropy is NOT the fix. Measured against real samples, a 32-char hex key
44
+ * scores 3.91 bits/char and that hostname scores 3.95: any threshold that
45
+ * catches the key also catches the hostname. What DOES separate them is the
46
+ * longest unbroken alphanumeric run, because human-readable identifiers are
47
+ * words joined by separators and credentials are not:
48
+ *
49
+ * hostname / path / branch / email .... longest run 7-8
50
+ * AWS secret key ...................... 13
51
+ * base64 blob, hex key ................ 30+
52
+ */
53
+ const OPAQUE_MIN_LENGTH = 24;
54
+ const OPAQUE_SHAPE = /^[A-Za-z0-9+/=_.-]+$/;
55
+ const OPAQUE_MIN_RUN = 12;
56
+ function longestAlphanumericRun(value) {
57
+ let longest = 0;
58
+ let current = 0;
59
+ for (const char of value) {
60
+ if (/[A-Za-z0-9]/.test(char)) {
61
+ current += 1;
62
+ longest = Math.max(longest, current);
63
+ }
64
+ else {
65
+ current = 0;
66
+ }
67
+ }
68
+ return longest;
69
+ }
70
+ /**
71
+ * Shapes that are addresses, not credentials. A URL, a hostname, an email and
72
+ * a filesystem path are exactly the connection hints the rail is meant to show,
73
+ * so they short-circuit the length backstop. They do NOT short-circuit the
74
+ * vendor patterns above: a JWT contains dots and is still a JWT.
75
+ */
76
+ function isHumanReadableHint(value) {
77
+ if (value.includes("://") || value.includes("@")) {
78
+ return true;
79
+ }
80
+ if (/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(value)) {
81
+ return true;
82
+ }
83
+ return value.startsWith("/") || value.startsWith("~/") || value.startsWith("./");
84
+ }
85
+ /** True when `value` looks like something that would still work if pasted. */
86
+ export function looksLikeLiveSecret(value) {
87
+ const trimmed = value.trim();
88
+ if (trimmed.length === 0) {
89
+ return false;
90
+ }
91
+ // Vendor prefixes are decisive and are checked first, so a token that happens
92
+ // to contain a dot or a slash is never argued out of being a token.
93
+ if (LIVE_SECRET_PATTERNS.some((pattern) => pattern.test(trimmed))) {
94
+ return true;
95
+ }
96
+ if (isHumanReadableHint(trimmed)) {
97
+ return false;
98
+ }
99
+ return (trimmed.length >= OPAQUE_MIN_LENGTH &&
100
+ OPAQUE_SHAPE.test(trimmed) &&
101
+ longestAlphanumericRun(trimmed) >= OPAQUE_MIN_RUN);
102
+ }
103
+ /** How many trailing characters a mask keeps so a human can tell two apart. */
104
+ const MASK_TAIL = 4;
105
+ const MASK_BULLETS = "••••";
106
+ /**
107
+ * Replace a value with something safe to store and render. Keeps a short tail
108
+ * so "is this the same token I put in the CI variable?" stays answerable, which
109
+ * is the only reason to show a credential row at all.
110
+ */
111
+ export function maskSecretValue(value) {
112
+ const trimmed = value.trim();
113
+ if (trimmed.length <= MASK_TAIL) {
114
+ return MASK_BULLETS;
115
+ }
116
+ return `${MASK_BULLETS}${trimmed.slice(-MASK_TAIL)}`;
117
+ }
118
+ /**
119
+ * The daemon-boundary call. Give it whatever the hook found; get back what is
120
+ * safe to persist, plus whether it had to be masked.
121
+ *
122
+ * `declaredMasked` lets a caller that already masked upstream say so, and is
123
+ * ignored when the value still looks live -- a caller cannot assert its way
124
+ * past the check.
125
+ */
126
+ export function normalizeSecretForStorage(value, declaredMasked) {
127
+ const trimmed = value.trim();
128
+ if (looksLikeLiveSecret(trimmed)) {
129
+ return { value: maskSecretValue(trimmed), masked: true };
130
+ }
131
+ return { value: trimmed, masked: declaredMasked ?? false };
132
+ }
133
+ //# sourceMappingURL=session-secrets.js.map