Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b1cf888
feat(storage): add x-goog-gcs-idempotency-token header linked to gccl…
thiyaguk09 Jul 3, 2026
5fe6e88
feat: allow user-provided x-goog-gcs-idempotency-token and synchroniz…
thiyaguk09 Jul 3, 2026
ce928a5
refactor: simplify sourcemap compilation scripts and add optional cha…
thiyaguk09 Jul 3, 2026
3004085
fix: ignore invalid empty or undefined idempotency tokens and fallbac…
thiyaguk09 Jul 3, 2026
550f83c
fix: remove user-provided token header when injecting GCS idempotency…
thiyaguk09 Jul 3, 2026
a25213f
fix: improve idempotency token validation and standardize token remov…
thiyaguk09 Jul 3, 2026
f0b6616
fix: perform immutable updates on customRequestOptions to avoid mutat…
thiyaguk09 Jul 9, 2026
e1c2e5a
Merge branch 'main' into feat/idempotency-tokens
thiyaguk09 Aug 25, 2026
853825d
Merge branch 'main' into feat/idempotency-tokens
thiyaguk09 Aug 26, 2026
0b9ad3d
Merge branch 'main' into feat/idempotency-tokens
thiyaguk09 Aug 27, 2026
8382760
Merge branch 'main' into feat/idempotency-tokens
thiyaguk09 Aug 27, 2026
9664d51
refactor: apply linting fixes to improve code formatting throughout s…
thiyaguk09 Aug 27, 2026
f67ecf9
Merge branch 'main' into feat/idempotency-tokens
thiyaguk09 Aug 31, 2026
7f258ee
feat: add x-goog-gcs-idempotency-token support to resumable uploads w…
thiyaguk09 Aug 31, 2026
39825bd
fix: update regex patterns to match non-whitespace characters and imp…
thiyaguk09 Aug 31, 2026
30c91c8
feat(storage): add IpFilter support to bucket metadata (#8623)
thiyaguk09 Aug 31, 2026
7079b46
Merge branch 'main' into feat/idempotency-tokens
thiyaguk09 Aug 31, 2026
855a638
chore: remove compile:sourcemaps script from package.json
thiyaguk09 Aug 31, 2026
14a3552
chore(storage): remove form-data dependency use native globals and up…
thiyaguk09 Aug 31, 2026
aec3f7e
Merge branch 'main' into feat/idempotency-tokens
thiyaguk09 Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions handwritten/storage/conformance-test/conformanceCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
* limitations under the License.
*/
import * as jsonToNodeApiMapping from './test-data/retryInvocationMap.json';
import * as libraryMethods from './libraryMethods';
import * as libraryMethods from './libraryMethods.js';
import {Bucket, File, HmacKey, Notification, Storage} from '../src/';
import * as crypto from 'crypto';
import * as assert from 'assert';
import {DecorateRequestOptions} from '../src/nodejs-common';
import fetch from 'node-fetch';

interface RetryCase {
instructions: String[];
Expand Down
4 changes: 1 addition & 3 deletions handwritten/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,8 @@
"@types/mocha": "^9.1.1",
"@types/mockery": "^1.4.29",
"@types/node": "^24.0.0",
"@types/node-fetch": "^2.1.3",
"@types/proxyquire": "^1.3.28",
"@types/request": "^2.48.4",
"@types/request": "^2.48.12",
"@types/sinon": "^17.0.0",
"@types/tmp": "0.2.6",
"@types/yargs": "^17.0.35",
Expand All @@ -115,7 +114,6 @@
"mocha": "^11.1.0",
"mockery": "^2.1.0",
"nock": "~13.5.0",
"node-fetch": "^2.6.7",
"pack-n-play": "^5.0.1",
"proxyquire": "^2.1.3",
"sinon": "^18.0.0",
Expand Down
42 changes: 42 additions & 0 deletions handwritten/storage/src/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,47 @@ export interface EncryptionEnforcementConfig {
restrictionMode?: 'NotRestricted' | 'FullyRestricted';
readonly effectiveTime?: string;
}

/**
* Configuration for a bucket's IP Filter.
*
* @example
* ```
* const {Storage} = require('@google-cloud/storage');
* const storage = new Storage();
* const bucket = storage.bucket('my-bucket');
*
* const metadata = {
* ipFilter: {
* mode: 'Enabled',
* publicNetworkSource: {
* allowedIpCidrRanges: ['192.168.1.1/32']
* }
* }
* };
*
* bucket.setMetadata(metadata, (err, apiResponse) => {
* if (err) {
* console.error(err);
* } else {
* console.log('IP filter updated successfully.');
* }
* });
* ```
*/
export interface IpFilter {
mode?: 'Enabled' | 'Disabled';
publicNetworkSource?: {
allowedIpCidrRanges?: string[];
};
vpcNetworkSources?: {
network?: string;
allowedIpCidrRanges?: string[];
}[];
allowAllServiceAgentAccess?: boolean;
allowCrossOrgVpcs?: boolean;
}

export interface BucketMetadata extends BaseMetadata {
acl?: AclMetadata[] | null;
autoclass?: {
Expand Down Expand Up @@ -341,6 +382,7 @@ export interface BucketMetadata extends BaseMetadata {
lockedTime?: string;
};
};
ipFilter?: IpFilter | null;
labels?: {
[key: string]: string | null;
};
Expand Down
1 change: 1 addition & 0 deletions handwritten/storage/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export {
GetNotificationsCallback,
GetNotificationsOptions,
GetNotificationsResponse,
IpFilter,
Labels,
LifecycleAction,
LifecycleCondition,
Expand Down
4 changes: 4 additions & 0 deletions handwritten/storage/src/nodejs-common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export {
AbortableDuplex,
ApiError,
BodyResponseCallback,
DecorateHeadersOptions,
DecorateHeadersResult,
DecorateRequestOptions,
decorateHeaders,
Headers,
ResponseBody,
util,
} from './util.js';
30 changes: 7 additions & 23 deletions handwritten/storage/src/nodejs-common/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
GoogleAuthOptions,
} from 'google-auth-library';
import type {Request} from 'teeny-request';
import * as crypto from 'crypto';

import {Interceptor} from './service-object.js';
import {
Expand All @@ -29,13 +28,9 @@ import {
GCCL_GCS_CMD_KEY,
MakeAuthenticatedRequest,
PackageJson,
decorateHeaders,
util,
} from './util.js';
import {
getRuntimeTrackingString,
getUserAgentString,
getModuleFormat,
} from '../util.js';

export const DEFAULT_PROJECT_ID_TOKEN = '{{projectId}}';

Expand Down Expand Up @@ -271,23 +266,12 @@ export class Service {

delete reqOpts.interceptors_;

const pkg = this.packageJson;
let userAgent = getUserAgentString();
if (this.providedUserAgent) {
userAgent = `${this.providedUserAgent} ${userAgent}`;
}
reqOpts.headers = {
...reqOpts.headers,
'User-Agent': userAgent,
'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${
pkg.version
}-${getModuleFormat()} gccl-invocation-id/${crypto.randomUUID()}`,
};

if (reqOpts[GCCL_GCS_CMD_KEY]) {
reqOpts.headers['x-goog-api-client'] +=
` gccl-gcs-cmd/${reqOpts[GCCL_GCS_CMD_KEY]}`;
}
const {headers} = decorateHeaders(reqOpts.headers, {
packageJson: this.packageJson,
providedUserAgent: this.providedUserAgent,
gcclGcsCmd: reqOpts[GCCL_GCS_CMD_KEY],
});
reqOpts.headers = headers;

if (reqOpts.shouldReturnStream) {
return this.makeAuthenticatedRequest(reqOpts) as {} as Request;
Expand Down
91 changes: 81 additions & 10 deletions handwritten/storage/src/nodejs-common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ const MAX_RETRY_DEFAULT = 3;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ResponseBody = any;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Headers = {[header: string]: any};

// Directly copy over Duplexify interfaces
export interface DuplexifyOptions extends DuplexOptions {
autoDestroy?: boolean;
Expand Down Expand Up @@ -1052,20 +1055,88 @@ export class Util {
: [optionsOrCallback as T, cb as C];
}

decorateHeaders(
headers?: CoreOptions['headers'],
options?: DecorateHeadersOptions
) {
return decorateHeaders(headers, options);
}

_getDefaultHeaders(gcclGcsCmd?: string) {
const headers = {
'User-Agent': getUserAgentString(),
'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${
packageJson.version
}-${getModuleFormat()} gccl-invocation-id/${crypto.randomUUID()}`,
};
return decorateHeaders(undefined, {gcclGcsCmd}).headers;
}
}

if (gcclGcsCmd) {
headers['x-goog-api-client'] += ` gccl-gcs-cmd/${gcclGcsCmd}`;
}
export interface DecorateHeadersOptions {
idempotencyToken?: string;
packageJson?: PackageJson;
providedUserAgent?: string;
gcclGcsCmd?: string;
}

export interface DecorateHeadersResult {
headers: Headers;
idempotencyToken: string;
}

return headers;
/**
* Decorates and sanitizes headers for GCS requests:
* - Checks for user-provided `x-goog-gcs-idempotency-token` case-insensitively.
* - If a valid non-empty string user token is provided, uses it as the idempotency token and preserves the header.
* - If not provided or invalid, removes any invalid header key and sets `x-goog-gcs-idempotency-token` to either the provided fallback token or a generated UUID.
* - Adds `User-Agent` and `x-goog-api-client` (with tracking string, package version, gccl-invocation-id, and optional gccl-gcs-cmd).
*
* @param headers Existing headers object (optional).
* @param options Decoration options (idempotencyToken, packageJson, providedUserAgent, gcclGcsCmd).
* @returns An object containing the decorated headers and the effective idempotency token.
*/
export function decorateHeaders(
headers?: CoreOptions['headers'],
options?: DecorateHeadersOptions
): DecorateHeadersResult {
const sanitizedHeaders: Headers = {...headers};
const userTokenKey = Object.keys(sanitizedHeaders).find(
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
);
const userTokenValue = userTokenKey
? sanitizedHeaders[userTokenKey]
: undefined;
const hasValidUserToken =
typeof userTokenValue === 'string' && userTokenValue.trim() !== '';

const idempotencyToken = hasValidUserToken
? (userTokenValue as string)
: options?.idempotencyToken || crypto.randomUUID();

let userAgent = getUserAgentString();
if (options?.providedUserAgent) {
userAgent = `${options.providedUserAgent} ${userAgent}`;
}

const pkg = options?.packageJson || packageJson;
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
pkg.version
}-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`;

const gcclGcsCmd = options?.gcclGcsCmd;
if (gcclGcsCmd) {
googAPIClient += ` gccl-gcs-cmd/${gcclGcsCmd}`;
}

sanitizedHeaders['User-Agent'] = userAgent;
sanitizedHeaders['x-goog-api-client'] = googAPIClient;

if (!hasValidUserToken) {
if (userTokenKey) {
delete sanitizedHeaders[userTokenKey];
}
sanitizedHeaders['x-goog-gcs-idempotency-token'] = idempotencyToken;
}

return {
headers: sanitizedHeaders,
idempotencyToken,
};
}

/**
Expand Down
Loading
Loading