@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895

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 (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +1 -2
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  5. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
  6. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  8. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  9. package/package.json +2 -3
  10. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  11. package/src/__tests__/local-standalone.test.ts +5 -36
  12. package/src/__tests__/managed-integration.test.ts +112 -91
  13. package/src/__tests__/managed-reconnect.test.ts +2 -2
  14. package/src/__tests__/transport.test.ts +23 -27
  15. package/src/cli.ts +1 -1
  16. package/src/index.ts +8 -88
  17. package/src/main.ts +228 -340
  18. package/src/paths.ts +4 -20
  19. package/src/server.ts +52 -469
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  21. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  22. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  23. package/src/__tests__/command-executor.test.ts +0 -1879
  24. package/src/__tests__/command-validator.test.ts +0 -1405
  25. package/src/__tests__/command-workspace.test.ts +0 -1050
  26. package/src/__tests__/grant-store.test.ts +0 -689
  27. package/src/__tests__/http-executor.test.ts +0 -1336
  28. package/src/__tests__/http-policy.test.ts +0 -1069
  29. package/src/__tests__/local-materializers.test.ts +0 -860
  30. package/src/__tests__/local-token-refresh.test.ts +0 -361
  31. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  32. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  33. package/src/__tests__/managed-materializers.test.ts +0 -1028
  34. package/src/__tests__/managed-rejection.test.ts +0 -43
  35. package/src/__tests__/toolstore.test.ts +0 -773
  36. package/src/audit/store.ts +0 -188
  37. package/src/commands/auth-adapters.ts +0 -169
  38. package/src/commands/egress-hooks.ts +0 -203
  39. package/src/commands/executor.ts +0 -1155
  40. package/src/commands/output-scan.ts +0 -157
  41. package/src/commands/profiles.ts +0 -286
  42. package/src/commands/validator.ts +0 -702
  43. package/src/commands/workspace.ts +0 -550
  44. package/src/grants/index.ts +0 -17
  45. package/src/grants/persistent-store.ts +0 -309
  46. package/src/grants/rpc-handlers.ts +0 -293
  47. package/src/grants/temporary-store.ts +0 -289
  48. package/src/http/audit.ts +0 -84
  49. package/src/http/executor.ts +0 -684
  50. package/src/http/path-template.ts +0 -245
  51. package/src/http/policy.ts +0 -238
  52. package/src/http/response-filter.ts +0 -233
  53. package/src/managed-errors.ts +0 -9
  54. package/src/managed-lazy-getters.ts +0 -106
  55. package/src/managed-main.ts +0 -822
  56. package/src/materializers/local-oauth-lookup.ts +0 -98
  57. package/src/materializers/local-token-refresh.ts +0 -287
  58. package/src/materializers/local.ts +0 -316
  59. package/src/materializers/managed-platform.ts +0 -295
  60. package/src/subjects/local.ts +0 -177
  61. package/src/subjects/managed.ts +0 -311
  62. package/src/subjects/policy.ts +0 -79
  63. package/src/toolstore/integrity.ts +0 -94
  64. package/src/toolstore/manifest.ts +0 -154
  65. package/src/toolstore/publish.ts +0 -571
@@ -1,311 +0,0 @@
1
- /**
2
- * Managed subject resolution for platform OAuth handles.
3
- *
4
- * Resolves `platform_oauth:<connection_id>` handles into a normalized
5
- * subject shape that the rest of CES can treat uniformly alongside local
6
- * subjects. Managed subjects never carry raw tokens — they carry only
7
- * the metadata needed to call the platform's token-materialization
8
- * endpoint at execution time.
9
- *
10
- * Subject resolution is the first phase of a two-phase credential flow:
11
- * 1. **Resolution** (this module) — parse the handle, validate the
12
- * connection exists in the platform catalog, and return a subject
13
- * descriptor with provider metadata.
14
- * 2. **Materialization** (`materializers/managed-platform.ts`) — use
15
- * the resolved subject to request a short-lived access token from
16
- * the platform and inject it into the execution environment.
17
- *
18
- * The subject shape is intentionally slim and secret-free so it can be
19
- * logged, cached in memory, and passed across internal boundaries without
20
- * risk of leaking credentials.
21
- */
22
-
23
- import {
24
- HandleType,
25
- parseHandle,
26
- type PlatformOAuthHandle,
27
- } from "@vellumai/service-contracts/credential-rpc";
28
-
29
- // ---------------------------------------------------------------------------
30
- // Common subject interface
31
- // ---------------------------------------------------------------------------
32
-
33
- /**
34
- * Source discriminator shared by all subject types.
35
- *
36
- * - `"local"` — credential lives in the local secure-key backend.
37
- * - `"managed"` — credential is managed by the platform; tokens are
38
- * obtained via the platform's CES token-materialization endpoint.
39
- */
40
- export type SubjectSource = "local" | "managed";
41
-
42
- /**
43
- * Common shape that all resolved subjects expose. CES execution paths
44
- * (HTTP materializer, command materializer) can branch on `source`
45
- * without knowing the full subject type.
46
- */
47
- export interface ResolvedSubject {
48
- /** Source of the credential. */
49
- source: SubjectSource;
50
- /** The raw handle string that was resolved. */
51
- handle: string;
52
- /** Provider identifier (e.g. "google", "slack", "github"). */
53
- provider: string;
54
- /** Connection identifier on the platform (managed) or locally. */
55
- connectionId: string;
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Managed subject shape
60
- // ---------------------------------------------------------------------------
61
-
62
- /**
63
- * A resolved managed subject — the output of resolving a
64
- * `platform_oauth:<connection_id>` handle against the platform catalog.
65
- *
66
- * This shape carries zero secret material. It is safe to log, serialize,
67
- * and pass across internal boundaries.
68
- */
69
- export interface ManagedSubject extends ResolvedSubject {
70
- source: "managed";
71
- /** Account info as reported by the platform catalog (e.g. email). */
72
- accountInfo: string | null;
73
- /** Granted OAuth scopes as reported by the platform catalog. */
74
- grantedScopes: string[];
75
- /** Connection status from the platform catalog (e.g. "active", "expired"). */
76
- status: string;
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // Platform catalog entry (non-secret subset from the platform response)
81
- // ---------------------------------------------------------------------------
82
-
83
- /**
84
- * Shape of a single connection entry in the platform catalog response.
85
- * Only non-secret fields are parsed; token values are never included.
86
- *
87
- * Field names match the platform's ManagedConnectionCatalogEntrySerializer:
88
- * handle, connection_id, provider, account_label, scopes_granted, status
89
- */
90
- export interface PlatformCatalogEntry {
91
- handle: string;
92
- connection_id: string;
93
- provider: string;
94
- account_label?: string | null;
95
- scopes_granted?: string[];
96
- status?: string;
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // Resolution errors
101
- // ---------------------------------------------------------------------------
102
-
103
- export class SubjectResolutionError extends Error {
104
- readonly code: string;
105
-
106
- constructor(code: string, message: string) {
107
- super(message);
108
- this.name = "SubjectResolutionError";
109
- this.code = code;
110
- }
111
- }
112
-
113
- // ---------------------------------------------------------------------------
114
- // Resolution options
115
- // ---------------------------------------------------------------------------
116
-
117
- export interface ManagedSubjectResolverOptions {
118
- /**
119
- * Platform base URL (without trailing slash).
120
- * e.g. "https://api.vellum.ai"
121
- */
122
- platformBaseUrl: string;
123
- /**
124
- * Assistant API key for authenticating with the platform.
125
- */
126
- assistantApiKey: string;
127
- /**
128
- * Platform-assigned assistant UUID. Required for building the
129
- * platform catalog URL: /v1/assistants/<id>/oauth/managed/catalog/
130
- */
131
- assistantId: string;
132
- /**
133
- * Optional custom fetch implementation (for testing).
134
- */
135
- fetch?: typeof globalThis.fetch;
136
- }
137
-
138
- // ---------------------------------------------------------------------------
139
- // Resolution result
140
- // ---------------------------------------------------------------------------
141
-
142
- export type ResolveResult =
143
- | { ok: true; subject: ManagedSubject }
144
- | { ok: false; error: SubjectResolutionError };
145
-
146
- // ---------------------------------------------------------------------------
147
- // Resolver implementation
148
- // ---------------------------------------------------------------------------
149
-
150
- /**
151
- * Resolve a `platform_oauth:<connection_id>` handle into a managed subject
152
- * by looking up the connection in the platform's CES catalog.
153
- *
154
- * Fail-closed: if the platform cannot be reached, returns an error rather
155
- * than proceeding without credential validation.
156
- */
157
- export async function resolveManagedSubject(
158
- handle: string,
159
- options: ManagedSubjectResolverOptions,
160
- ): Promise<ResolveResult> {
161
- // -- Parse handle ---------------------------------------------------------
162
- const parsed = parseHandle(handle);
163
- if (!parsed.ok) {
164
- return {
165
- ok: false,
166
- error: new SubjectResolutionError("INVALID_HANDLE", parsed.error),
167
- };
168
- }
169
-
170
- if (parsed.handle.type !== HandleType.PlatformOAuth) {
171
- return {
172
- ok: false,
173
- error: new SubjectResolutionError(
174
- "WRONG_HANDLE_TYPE",
175
- `Expected platform_oauth handle, got ${parsed.handle.type}`,
176
- ),
177
- };
178
- }
179
-
180
- const platformHandle = parsed.handle as PlatformOAuthHandle;
181
-
182
- // -- Validate prerequisites -----------------------------------------------
183
- if (!options.platformBaseUrl) {
184
- return {
185
- ok: false,
186
- error: new SubjectResolutionError(
187
- "MISSING_PLATFORM_URL",
188
- "Platform base URL is required for managed subject resolution",
189
- ),
190
- };
191
- }
192
-
193
- if (!options.assistantApiKey) {
194
- return {
195
- ok: false,
196
- error: new SubjectResolutionError(
197
- "MISSING_API_KEY",
198
- "Assistant API key is required for managed subject resolution",
199
- ),
200
- };
201
- }
202
-
203
- if (!options.assistantId) {
204
- return {
205
- ok: false,
206
- error: new SubjectResolutionError(
207
- "MISSING_ASSISTANT_ID",
208
- "Assistant ID is required for managed subject resolution",
209
- ),
210
- };
211
- }
212
-
213
- // -- Fetch catalog entry --------------------------------------------------
214
- const fetchFn = options.fetch ?? globalThis.fetch;
215
- const catalogUrl = `${options.platformBaseUrl}/v1/assistants/${encodeURIComponent(options.assistantId)}/oauth/managed/catalog/`;
216
-
217
- let response: Response;
218
- try {
219
- response = await fetchFn(catalogUrl, {
220
- method: "GET",
221
- headers: {
222
- Authorization: `Api-Key ${options.assistantApiKey}`,
223
- Accept: "application/json",
224
- },
225
- });
226
- } catch (err) {
227
- const message = err instanceof Error ? err.message : String(err);
228
- return {
229
- ok: false,
230
- error: new SubjectResolutionError(
231
- "PLATFORM_UNREACHABLE",
232
- `Failed to reach platform CES catalog: ${sanitizeError(message)}`,
233
- ),
234
- };
235
- }
236
-
237
- if (!response.ok) {
238
- return {
239
- ok: false,
240
- error: new SubjectResolutionError(
241
- `PLATFORM_HTTP_${response.status}`,
242
- `Platform CES catalog returned HTTP ${response.status}`,
243
- ),
244
- };
245
- }
246
-
247
- // -- Parse response -------------------------------------------------------
248
- // The platform returns a flat JSON array of catalog entries
249
- // (serialized with many=True), not a wrapper object.
250
- let entries: PlatformCatalogEntry[];
251
- try {
252
- entries = (await response.json()) as PlatformCatalogEntry[];
253
- } catch {
254
- return {
255
- ok: false,
256
- error: new SubjectResolutionError(
257
- "INVALID_CATALOG_RESPONSE",
258
- "Platform CES catalog returned invalid JSON",
259
- ),
260
- };
261
- }
262
-
263
- if (!Array.isArray(entries)) {
264
- return {
265
- ok: false,
266
- error: new SubjectResolutionError(
267
- "INVALID_CATALOG_RESPONSE",
268
- "Platform CES catalog returned unexpected response format",
269
- ),
270
- };
271
- }
272
-
273
- // -- Find matching connection ---------------------------------------------
274
- const entry = entries.find(
275
- (c) => c.connection_id === platformHandle.connectionId,
276
- );
277
-
278
- if (!entry) {
279
- return {
280
- ok: false,
281
- error: new SubjectResolutionError(
282
- "CONNECTION_NOT_FOUND",
283
- `Connection ${platformHandle.connectionId} not found in platform catalog`,
284
- ),
285
- };
286
- }
287
-
288
- // -- Build managed subject ------------------------------------------------
289
- const subject: ManagedSubject = {
290
- source: "managed",
291
- handle,
292
- provider: entry.provider,
293
- connectionId: entry.connection_id,
294
- accountInfo: entry.account_label ?? null,
295
- grantedScopes: entry.scopes_granted ?? [],
296
- status: entry.status ?? "unknown",
297
- };
298
-
299
- return { ok: true, subject };
300
- }
301
-
302
- // ---------------------------------------------------------------------------
303
- // Helpers
304
- // ---------------------------------------------------------------------------
305
-
306
- /**
307
- * Sanitize error messages to avoid leaking secrets (defensive).
308
- */
309
- function sanitizeError(message: string): string {
310
- return message.replace(/Api-Key\s+\S+/gi, "Api-Key [REDACTED]");
311
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * CES credential policy enforcement.
3
- *
4
- * Enforces credential-level policies (allowedTools, allowedDomains) that were
5
- * previously only checked by the pre-CES broker in the assistant daemon.
6
- * Without these checks, CES would materialise credentials that the broker
7
- * would have rejected — a security regression.
8
- *
9
- * Policy rules:
10
- * - **allowedDomains** — credentials with domain restrictions are scoped to
11
- * browser use on those domains and cannot be used server-side by CES.
12
- * - **allowedTools** — if set (even if empty), only the listed tools may
13
- * consume the credential. An empty array means deny-all. CES tool names
14
- * ("make_authenticated_request", "run_authenticated_command") must be in
15
- * the list.
16
- */
17
-
18
- import type { StaticCredentialRecord } from "@vellumai/credential-storage";
19
-
20
- // ---------------------------------------------------------------------------
21
- // Result type
22
- // ---------------------------------------------------------------------------
23
-
24
- export interface CredentialPolicyCheckResult {
25
- ok: boolean;
26
- error?: string;
27
- }
28
-
29
- // ---------------------------------------------------------------------------
30
- // Policy check
31
- // ---------------------------------------------------------------------------
32
-
33
- /**
34
- * Check credential-level policies before materialisation.
35
- *
36
- * Returns `{ ok: true }` if the credential may be materialised for the
37
- * given CES tool, or `{ ok: false, error }` with a human-readable
38
- * rejection message.
39
- *
40
- * @param metadata Non-secret metadata record for a local static credential.
41
- * @param cesToolName The CES tool requesting materialisation
42
- * (e.g. "make_authenticated_request" or "run_authenticated_command").
43
- */
44
- export function checkCredentialPolicy(
45
- metadata: StaticCredentialRecord,
46
- cesToolName: string,
47
- ): CredentialPolicyCheckResult {
48
- // -- allowedDomains -------------------------------------------------------
49
- // Credentials with domain restrictions are scoped to browser use on those
50
- // domains. They cannot be used for server-side operations through CES.
51
- if (metadata.allowedDomains && metadata.allowedDomains.length > 0) {
52
- return {
53
- ok: false,
54
- error:
55
- `Credential ${metadata.service}/${metadata.field} has domain restrictions ` +
56
- `(${metadata.allowedDomains.join(", ")}) and cannot be used for server-side ` +
57
- `operations through CES. Remove domain restrictions or use a separate credential.`,
58
- };
59
- }
60
-
61
- // -- allowedTools ---------------------------------------------------------
62
- // If set (even if empty), the CES tool must be in the allowed list.
63
- // An empty allowedTools array means no tools are permitted (deny-all).
64
- if (metadata.allowedTools) {
65
- if (!metadata.allowedTools.includes(cesToolName)) {
66
- return {
67
- ok: false,
68
- error:
69
- metadata.allowedTools.length === 0
70
- ? `Credential ${metadata.service}/${metadata.field} does not allow any tools.`
71
- : `Tool "${cesToolName}" is not allowed to use credential ` +
72
- `${metadata.service}/${metadata.field}. ` +
73
- `Allowed tools: ${metadata.allowedTools.join(", ")}.`,
74
- };
75
- }
76
- }
77
-
78
- return { ok: true };
79
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Bundle integrity verification.
3
- *
4
- * Provides SHA-256 digest computation and verification for secure command
5
- * bundles. Digests are computed over the raw bundle bytes and compared
6
- * against the expected digest declared in the toolstore manifest.
7
- *
8
- * All digests are lowercase hex-encoded SHA-256 hashes (64 characters).
9
- */
10
-
11
- import { createHash, timingSafeEqual } from "node:crypto";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Digest computation
15
- // ---------------------------------------------------------------------------
16
-
17
- /**
18
- * Compute the SHA-256 hex digest of arbitrary bytes.
19
- *
20
- * Returns a lowercase 64-character hex string.
21
- */
22
- export function computeDigest(data: Buffer | Uint8Array): string {
23
- return createHash("sha256").update(data).digest("hex");
24
- }
25
-
26
- // ---------------------------------------------------------------------------
27
- // Digest verification
28
- // ---------------------------------------------------------------------------
29
-
30
- export interface DigestVerificationResult {
31
- /** Whether the computed digest matches the expected digest. */
32
- valid: boolean;
33
- /** The computed digest (always present). */
34
- computedDigest: string;
35
- /** The expected digest (always present). */
36
- expectedDigest: string;
37
- /** Human-readable error message when invalid (undefined when valid). */
38
- error?: string;
39
- }
40
-
41
- /**
42
- * Verify that the SHA-256 digest of `data` matches `expectedDigest`.
43
- *
44
- * Uses constant-time comparison via `timingSafeEqual` to prevent
45
- * timing side-channel attacks on digest values.
46
- */
47
- export function verifyDigest(
48
- data: Buffer | Uint8Array,
49
- expectedDigest: string,
50
- ): DigestVerificationResult {
51
- const computedDigest = computeDigest(data);
52
-
53
- // Use timing-safe comparison to prevent timing attacks
54
- const computedBuf = Buffer.from(computedDigest, "hex");
55
- const expectedBuf = Buffer.from(expectedDigest, "hex");
56
-
57
- // If the expected digest is not valid hex (wrong length), fail
58
- if (computedBuf.length !== expectedBuf.length || expectedBuf.length !== 32) {
59
- return {
60
- valid: false,
61
- computedDigest,
62
- expectedDigest,
63
- error: `Digest mismatch: expected "${expectedDigest}" but computed "${computedDigest}". ` +
64
- `The bundle contents do not match the declared digest.`,
65
- };
66
- }
67
-
68
- const match = safeTimingEqual(computedBuf, expectedBuf);
69
-
70
- if (!match) {
71
- return {
72
- valid: false,
73
- computedDigest,
74
- expectedDigest,
75
- error: `Digest mismatch: expected "${expectedDigest}" but computed "${computedDigest}". ` +
76
- `The bundle contents do not match the declared digest.`,
77
- };
78
- }
79
-
80
- return {
81
- valid: true,
82
- computedDigest,
83
- expectedDigest,
84
- };
85
- }
86
-
87
- /**
88
- * Constant-time buffer comparison. Wraps `crypto.timingSafeEqual`
89
- * with a length guard (timingSafeEqual throws on length mismatch).
90
- */
91
- function safeTimingEqual(a: Buffer, b: Buffer): boolean {
92
- if (a.length !== b.length) return false;
93
- return timingSafeEqual(a, b);
94
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * Toolstore manifest type definitions.
3
- *
4
- * Describes an approved secure command bundle that CES can publish into
5
- * its private immutable toolstore. Each manifest records:
6
- *
7
- * - **sourceUrl** — The canonical URL the bundle was fetched from.
8
- * - **expectedDigest** — SHA-256 hex digest that the downloaded bytes
9
- * must match before publication.
10
- * - **bundleId** — Unique identifier for the command bundle
11
- * (e.g. "gh-cli", "aws-cli").
12
- * - **version** — Semantic version of the bundle.
13
- * - **commandProfiles** — Profile names from the secure command manifest
14
- * that this bundle declares.
15
- *
16
- * ## Publishing rules
17
- *
18
- * 1. **Only CES can write** — bundles are published into the CES-private
19
- * data root, which the assistant process cannot reach.
20
- * 2. **Immutable once published** — a bundle directory keyed by its digest
21
- * is never overwritten. Re-publishing the same digest is a no-op.
22
- * 3. **Workspace-origin binaries are never publishable** — bundles must
23
- * come from a known source URL; arbitrary assistant-provided bytes are
24
- * rejected.
25
- * 4. **Publication does not grant credentials** — writing a bundle into
26
- * the toolstore is purely a content operation. Credential-use grants
27
- * are managed by a separate subsystem.
28
- */
29
-
30
- import type { SecureCommandManifest } from "../commands/profiles.js";
31
-
32
- // ---------------------------------------------------------------------------
33
- // Bundle origin
34
- // ---------------------------------------------------------------------------
35
-
36
- /**
37
- * Describes the provenance of a bundle.
38
- *
39
- * `sourceUrl` must be an HTTPS URL. Workspace paths, file:// URLs, and
40
- * data: URLs are structurally rejected.
41
- */
42
- export interface BundleOrigin {
43
- /** HTTPS URL from which the bundle was fetched. */
44
- sourceUrl: string;
45
- /** ISO-8601 timestamp of when the bundle was fetched. */
46
- fetchedAt: string;
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // Toolstore manifest
51
- // ---------------------------------------------------------------------------
52
-
53
- /**
54
- * A toolstore manifest describes a single approved secure command bundle.
55
- *
56
- * This is the metadata stored alongside the bundle contents in the
57
- * content-addressed toolstore directory.
58
- */
59
- export interface ToolstoreManifest {
60
- /** SHA-256 hex digest of the bundle contents. Content-address key. */
61
- digest: string;
62
-
63
- /** Unique identifier for the command bundle. */
64
- bundleId: string;
65
-
66
- /** Semantic version of the bundle. */
67
- version: string;
68
-
69
- /** Provenance information — where the bundle came from. */
70
- origin: BundleOrigin;
71
-
72
- /** Profile names declared in the secure command manifest. */
73
- declaredProfiles: string[];
74
-
75
- /**
76
- * The full secure command manifest embedded in the toolstore manifest.
77
- * Used for runtime validation without needing to re-parse the bundle.
78
- */
79
- secureCommandManifest: SecureCommandManifest;
80
-
81
- /** ISO-8601 timestamp of when the bundle was published to the toolstore. */
82
- publishedAt: string;
83
- }
84
-
85
- // ---------------------------------------------------------------------------
86
- // Validation helpers
87
- // ---------------------------------------------------------------------------
88
-
89
- /** Regex for a valid SHA-256 hex digest. */
90
- const SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
91
-
92
- /**
93
- * Returns true if the given string is a valid SHA-256 hex digest.
94
- */
95
- export function isValidSha256Hex(digest: string): boolean {
96
- return SHA256_HEX_PATTERN.test(digest);
97
- }
98
-
99
- /**
100
- * Schemes that are structurally rejected as bundle source URLs.
101
- * Only HTTPS sources are accepted.
102
- */
103
- const REJECTED_URL_SCHEMES = ["file:", "data:", "blob:", "javascript:"];
104
-
105
- /**
106
- * Validate a source URL for bundle origin.
107
- *
108
- * Accepted: HTTPS URLs only.
109
- * Rejected: file://, data://, workspace paths, non-URL strings.
110
- *
111
- * Returns an error message if invalid, or null if valid.
112
- */
113
- export function validateSourceUrl(sourceUrl: string): string | null {
114
- if (!sourceUrl || sourceUrl.trim().length === 0) {
115
- return "sourceUrl is required and must be non-empty.";
116
- }
117
-
118
- // Must be a valid URL
119
- let parsed: URL;
120
- try {
121
- parsed = new URL(sourceUrl);
122
- } catch {
123
- return `sourceUrl "${sourceUrl}" is not a valid URL. Only HTTPS URLs are accepted.`;
124
- }
125
-
126
- // Must be HTTPS
127
- if (parsed.protocol !== "https:") {
128
- if (REJECTED_URL_SCHEMES.includes(parsed.protocol)) {
129
- return `sourceUrl scheme "${parsed.protocol}" is not allowed. Only HTTPS URLs are accepted as bundle sources.`;
130
- }
131
- return `sourceUrl scheme "${parsed.protocol}" is not allowed. Only HTTPS URLs are accepted.`;
132
- }
133
-
134
- return null;
135
- }
136
-
137
- /**
138
- * Workspace-origin path patterns that are never publishable as bundle
139
- * sources. These catch attempts to publish assistant-provided bytes
140
- * directly from the workspace directory.
141
- */
142
- const WORKSPACE_PATH_PATTERNS = [
143
- /^~?\/?\.vellum\//,
144
- /\/\.vellum\//,
145
- /\/workspace\//i,
146
- ] as const;
147
-
148
- /**
149
- * Returns true if the given path looks like a workspace-origin path
150
- * that should never be accepted as a bundle source.
151
- */
152
- export function isWorkspaceOriginPath(path: string): boolean {
153
- return WORKSPACE_PATH_PATTERNS.some((pattern) => pattern.test(path));
154
- }