diff --git a/handwritten/storage/src/file.ts b/handwritten/storage/src/file.ts index 27765d935a99..786998c5f4e4 100644 --- a/handwritten/storage/src/file.ts +++ b/handwritten/storage/src/file.ts @@ -148,21 +148,94 @@ export interface SignedPostPolicyV4Output { url: string; fields: PolicyFields; } - export interface GetSignedUrlConfig extends Pick { + /** + * The action to permit with the signed URL. + * - `'read'`: Allows downloading/viewing the file (HTTP GET). + * - `'write'`: Allows uploading/overwriting the file (HTTP PUT). + * - `'delete'`: Allows removing the file (HTTP DELETE). + * - `'resumable'`: Allows resumable uploads (HTTP POST). + * Note: When using `'resumable'`, the header `X-Goog-Resumable: start` must be sent in the client request. + */ action: 'read' | 'write' | 'delete' | 'resumable'; + + /** + * The signing version to use. + * @default 'v2' + */ version?: 'v2' | 'v4'; + + /** + * Determines the URL structure for accessing bucket resources. + * - `true`: Uses virtual hosted-style URLs (e.g., `https://mybucket.storage.googleapis.com/...`) + * - `false`: Uses path-style URLs (e.g., `https://storage.googleapis.com/mybucket/...`). + * Virtual hosted-style URLs are generally preferred. + * @default false + */ virtualHostedStyle?: boolean; + + /** + * The custom domain name (CNAME) mapped to this bucket (e.g., `"https://cdn.example.com"`). + */ cname?: string; + + /** + * The MD5 digest value in base64. If provided, the client request **must** + * include an identical `Content-MD5` HTTP header. + * If omitted, the client request must not include this header. + */ contentMd5?: string; + + /** + * The expected Content-Type of the file. If provided, the client request **must** + * include an identical `Content-Type` HTTP header. + * If omitted, the client request must not include this header. + */ contentType?: string; + + /** + * The expiration timestamp for the link. Any provided value is passed directly to `new Date()`. + * @throws {Error} If an expiration timestamp from the past is given. + * Note: `'v4'` signing supports a maximum duration of 7 days (604,800 seconds) from the creation time. + */ expires: string | number | Date; + + /** + * The timestamp when this link becomes usable. Any provided value is passed directly to `new Date()`. + * @default Date.now() + * Note: Only supported/applicable when `version` is set to `'v4'`. + */ accessibleAt?: string | number | Date; + + /** + * Canonical extension headers that the server will validate against the client's request. + * Requirements: + * - Header names must be prefixed with `x-goog-` and must be entirely lowercase. + * - Multi-valued headers passed as an array are converted into a comma-separated string (no spaces). + * The client must format them identically to prevent signature mismatches. + */ extensionHeaders?: http.OutgoingHttpHeaders; + + /** + * The filename to prompt the browser/user to save the file as upon access. + * Note: This option is ignored if `responseDisposition` is explicitly set. + */ promptSaveAs?: string; + + /** + * Maps to the `response-content-disposition` query parameter in the signed URL. + */ responseDisposition?: string; + + /** + * Maps to the `response-content-type` query parameter in the signed URL. + */ responseType?: string; + + /** + * Additional query parameters to include natively in the generated signed URL. + */ queryParams?: Query; } @@ -3236,6 +3309,7 @@ class File extends ServiceObject { contentMd5: cfg.contentMd5, contentType: cfg.contentType, host: cfg.host, + signingEndpoint: cfg.signingEndpoint, }; if (cfg.cname) { diff --git a/handwritten/storage/src/signer.ts b/handwritten/storage/src/signer.ts index a657cef6133d..ba5c17c04b75 100644 --- a/handwritten/storage/src/signer.ts +++ b/handwritten/storage/src/signer.ts @@ -25,7 +25,7 @@ type GoogleAuthLike = Pick; * @deprecated Use {@link GoogleAuth} instead */ export interface AuthClient { - sign(blobToSign: string): Promise; + sign(blobToSign: string, signingEndpoint?: string): Promise; getCredentials(): Promise<{ client_email?: string; }>; diff --git a/handwritten/storage/test/file.ts b/handwritten/storage/test/file.ts index 26823995b907..d9f9185a16e8 100644 --- a/handwritten/storage/test/file.ts +++ b/handwritten/storage/test/file.ts @@ -3794,11 +3794,30 @@ describe('File', () => { contentType: config.contentType, cname: CNAME, virtualHostedStyle: true, + signingEndpoint: undefined, }); done(); }); }); + it('should pass signingEndpoint to URLSigner', done => { + const signingEndpoint = 'https://my-endpoint.com'; + const config = { + ...SIGNED_URL_CONFIG, + signingEndpoint, + }; + + file.getSignedUrl(config, (err: Error | null) => { + assert.ifError(err); + const getSignedUrlArgs = signerGetSignedUrlStub.getCall(0).args; + assert.strictEqual( + getSignedUrlArgs[0]['signingEndpoint'], + signingEndpoint + ); + done(); + }); + }); + it('should add "x-goog-resumable: start" header if action is resumable', done => { SIGNED_URL_CONFIG.action = 'resumable'; SIGNED_URL_CONFIG.extensionHeaders = {