@vellumai/credential-executor 0.10.11-dev.202607222325.35fc367 → 0.10.11-dev.202607230140.b414d01

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.
@@ -100,3 +100,93 @@ export interface RemoteWebPairingTokenApprovedResponse {
100
100
  export type RemoteWebPairingTokenResponse =
101
101
  | RemoteWebPairingTokenPendingResponse
102
102
  | RemoteWebPairingTokenApprovedResponse;
103
+
104
+ // ── Shared pairing URL helpers ──────────────────────────────────────────────
105
+ //
106
+ // Every surface that mints a pairing (the `vellum pair --qr` CLI, the web
107
+ // settings "Pair a device" card) must accept the same public URLs and build
108
+ // the same scannable links. The helpers are environment-neutral (WHATWG URL
109
+ // only) so both Node and browser callers share one implementation.
110
+
111
+ /** Why a public base URL can't be advertised in a pairing challenge. */
112
+ export type PublicBaseUrlRejection = "unparseable" | "loopback" | "non-https";
113
+
114
+ export type PublicBaseUrlResult =
115
+ | { ok: true; url: string }
116
+ | { ok: false; reason: PublicBaseUrlRejection };
117
+
118
+ /**
119
+ * A loopback URL — `localhost`, `[::1]`, or `127.x.x.x`. A pairing link that
120
+ * encodes a loopback address is unreachable from the scanning device.
121
+ */
122
+ export function isLoopbackPublicUrl(url: string): boolean {
123
+ try {
124
+ // WHATWG URL canonicalizes hostnames, so IPv6 loopback is always "[::1]".
125
+ const hostname = new URL(url).hostname;
126
+ return (
127
+ hostname === "localhost" ||
128
+ hostname === "[::1]" ||
129
+ /^127(?:\.\d{1,3}){3}$/.test(hostname)
130
+ );
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Normalize an address to the public base a scanning device opens:
138
+ * query/hash stripped, the `assistant` path segment (and everything after it)
139
+ * removed so a pasted pair-page URL collapses to its base, and trailing
140
+ * slashes trimmed. Throws if the value is not a parseable URL.
141
+ */
142
+ export function normalizePublicBaseUrl(value: string): string {
143
+ const url = new URL(value);
144
+ url.search = "";
145
+ url.hash = "";
146
+ const parts = url.pathname.split("/").filter(Boolean);
147
+ const assistantIndex = parts.indexOf("assistant");
148
+ if (assistantIndex >= 0) {
149
+ parts.splice(assistantIndex);
150
+ }
151
+ url.pathname = parts.length ? `/${parts.join("/")}` : "/";
152
+ return url.toString().replace(/\/+$/, "");
153
+ }
154
+
155
+ /**
156
+ * Resolve an address to the public https base URL to advertise in a pairing
157
+ * challenge, or report why it can't be used. Loopback and non-https links are
158
+ * refused with a specific reason callers turn into their own guidance.
159
+ */
160
+ export function resolvePublicBaseUrl(raw: string): PublicBaseUrlResult {
161
+ let normalized: string;
162
+ try {
163
+ normalized = normalizePublicBaseUrl(raw);
164
+ } catch {
165
+ return { ok: false, reason: "unparseable" };
166
+ }
167
+ if (isLoopbackPublicUrl(normalized)) {
168
+ return { ok: false, reason: "loopback" };
169
+ }
170
+ if (new URL(normalized).protocol !== "https:") {
171
+ return { ok: false, reason: "non-https" };
172
+ }
173
+ return { ok: true, url: normalized };
174
+ }
175
+
176
+ /**
177
+ * The scannable pair URL: the challenge's verification URI with the device
178
+ * code carried in the fragment (`#device_code=…`), matching what the pair
179
+ * page reads on load. Fragments never reach the wire.
180
+ */
181
+ export function buildRemoteWebPairingUrl(
182
+ challenge: Pick<
183
+ RemoteWebPairingChallengeResponse,
184
+ "verificationUri" | "deviceCode"
185
+ >,
186
+ ): string {
187
+ const url = new URL(challenge.verificationUri);
188
+ url.hash = new URLSearchParams({
189
+ device_code: challenge.deviceCode,
190
+ }).toString();
191
+ return url.toString();
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/credential-executor",
3
- "version": "0.10.11-dev.202607222325.35fc367",
3
+ "version": "0.10.11-dev.202607230140.b414d01",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {