chrome-webstore-upload 4.0.2 → 5.0.0-0

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.
@@ -22,18 +22,10 @@ function parseErrorMessage(response) {
22
22
  return error.message.replace(/^Publish condition not met: /, '');
23
23
  }
24
24
  }
25
- // Handle item errors in ItemResource: { itemError: [{ error_code: "...", error_detail: "..." }] }
26
- const itemResource = response;
27
- if (itemResource.itemError && Array.isArray(itemResource.itemError) && itemResource.itemError.length > 0) {
28
- const errorDetails = itemResource.itemError.map(error => error.error_detail).join('; ');
29
- return errorDetails;
30
- }
31
25
  return 'Unknown error';
32
26
  }
33
27
  export function throwIfNotOk(request, response) {
34
- // Check for upload failure even on HTTP 200
35
- const itemResource = response;
36
- if (!request.ok || itemResource.uploadState === 'FAILURE') {
28
+ if (!request.ok) {
37
29
  const message = parseErrorMessage(response);
38
30
  const error = new CWSError(message);
39
31
  error.cause = response;
@@ -1,22 +1,29 @@
1
1
  import { type ReadStream } from 'node:fs';
2
- import type { APIClientOptions, ItemResource, PublishResponse } from './types.js';
2
+ import type { APIClientOptions, ItemResource, ItemStatusResponse, PublishResponse, PublishType } from './types.js';
3
3
  export declare const refreshTokenURI = "https://www.googleapis.com/oauth2/v4/token";
4
- export type { APIClientOptions, ItemResource, PublishResponse } from './types.js';
4
+ export type { APIClientOptions, ItemResource, ItemStatusResponse, PublishResponse, PublishType, } from './types.js';
5
5
  export { CWSError } from './errors.js';
6
6
  declare class APIClient {
7
7
  extensionId: string;
8
+ publisherId: string;
8
9
  clientId: string;
9
10
  refreshToken: string;
10
11
  clientSecret: string | undefined;
11
12
  constructor(options: APIClientOptions);
12
13
  uploadExisting(streamOrPath: ReadStream | ReadableStream | string, token?: string | Promise<string>, maxAwaitInProgressResponseSeconds?: number): Promise<ItemResource>;
13
- publish(target?: string, token?: string | Promise<string>, deployPercentage?: number | undefined): Promise<PublishResponse>;
14
- get(projection?: string, token?: string | Promise<string>): Promise<ItemResource>;
14
+ publish(publishType?: PublishType | 'default' | 'trustedTesters', token?: string | Promise<string>, deployPercentage?: number | undefined): Promise<PublishResponse>;
15
+ setDeployPercentage(deployPercentage: number, token?: string | Promise<string>): Promise<void>;
16
+ get(token?: string | Promise<string>): Promise<ItemStatusResponse>;
15
17
  fetchToken(): Promise<string>;
16
18
  _waitUploadSuccess(response: ItemResource, maxAwaitInProgressResponseSeconds: number): Promise<ItemResource>;
19
+ _normalizePublishType(target: PublishType | 'default' | 'trustedTesters'): PublishType;
17
20
  _headers(token: string): {
18
21
  Authorization: string;
19
- 'x-goog-api-version': string;
22
+ };
23
+ _uploadHeaders(token: string, fileName: string): {
24
+ Authorization: string;
25
+ 'X-Goog-Upload-Protocol': string;
26
+ 'X-Goog-Upload-File-Name': string;
20
27
  };
21
28
  }
22
29
  export default function chromeWebstoreUpload(options: APIClientOptions): APIClient;
@@ -4,19 +4,14 @@
4
4
  import fs from 'node:fs';
5
5
  import { throwIfNotOk } from './errors.js';
6
6
  import zipStreamFromDirectory from './zip-dir.js';
7
- const rootURI = 'https://www.googleapis.com';
7
+ const rootURI = 'https://chromewebstore.googleapis.com';
8
8
  export const refreshTokenURI = 'https://www.googleapis.com/oauth2/v4/token';
9
- const uploadExistingURI = (id) => `${rootURI}/upload/chromewebstore/v1.1/items/${id}`;
10
- const publishURI = ({ extensionId, target = 'default', deployPercentage }) => {
11
- const url = new URL(`${rootURI}/chromewebstore/v1.1/items/${extensionId}/publish`);
12
- url.searchParams.set('publishTarget', target);
13
- if (deployPercentage !== undefined) {
14
- url.searchParams.set('deployPercentage', String(deployPercentage));
15
- }
16
- return url.href;
17
- };
18
- const getURI = (id, projection) => `${rootURI}/chromewebstore/v1.1/items/${id}?projection=${projection}`;
19
- const requiredFields = ['extensionId', 'clientId', 'refreshToken'];
9
+ const itemName = (publisherId, extensionId) => `publishers/${publisherId}/items/${extensionId}`;
10
+ const uploadExistingURI = (publisherId, extensionId) => `${rootURI}/upload/v2/${itemName(publisherId, extensionId)}:upload`;
11
+ const publishURI = (publisherId, extensionId) => `${rootURI}/v2/${itemName(publisherId, extensionId)}:publish`;
12
+ const fetchStatusURI = (publisherId, extensionId) => `${rootURI}/v2/${itemName(publisherId, extensionId)}:fetchStatus`;
13
+ const setDeployPercentageURI = (publisherId, extensionId) => `${rootURI}/v2/${itemName(publisherId, extensionId)}:setPublishedDeployPercentage`;
14
+ const requiredFields = ['extensionId', 'publisherId', 'clientId', 'refreshToken'];
20
15
  const retryIntervalSeconds = 2;
21
16
  async function getStreamFromPath(filepath) {
22
17
  const stats = await fs.promises.stat(filepath);
@@ -27,6 +22,7 @@ async function getStreamFromPath(filepath) {
27
22
  export { CWSError } from './errors.js';
28
23
  class APIClient {
29
24
  extensionId;
25
+ publisherId;
30
26
  clientId;
31
27
  refreshToken;
32
28
  clientSecret;
@@ -43,6 +39,7 @@ class APIClient {
43
39
  }
44
40
  }
45
41
  this.extensionId = options.extensionId;
42
+ this.publisherId = options.publisherId;
46
43
  this.clientId = options.clientId;
47
44
  this.refreshToken = options.refreshToken;
48
45
  this.clientSecret = options.clientSecret;
@@ -52,13 +49,14 @@ class APIClient {
52
49
  throw new Error('Read stream missing');
53
50
  }
54
51
  // Convert string path (file or directory) to stream
52
+ const fileName = typeof streamOrPath === 'string' && streamOrPath.endsWith('.crx') ? 'extension.crx' : 'extension.zip';
55
53
  const readStream = typeof streamOrPath === 'string'
56
54
  ? await getStreamFromPath(streamOrPath)
57
55
  : streamOrPath;
58
- const { extensionId } = this;
59
- const request = await fetch(uploadExistingURI(extensionId), {
60
- method: 'PUT',
61
- headers: this._headers(await token),
56
+ const { extensionId, publisherId } = this;
57
+ const request = await fetch(uploadExistingURI(publisherId, extensionId), {
58
+ method: 'POST',
59
+ headers: this._uploadHeaders(await token, fileName),
62
60
  // @ts-expect-error Node extension? 🤷‍♂️ Required https://github.com/nodejs/node/issues/46221
63
61
  duplex: 'half',
64
62
  // Until they figure it out, this seems to work. Alternatively use https://stackoverflow.com/a/76780381/288906
@@ -68,19 +66,42 @@ class APIClient {
68
66
  throwIfNotOk(request, response);
69
67
  return this._waitUploadSuccess(response, maxAwaitInProgressResponseSeconds);
70
68
  }
71
- async publish(target = 'default', token = this.fetchToken(), deployPercentage = undefined) {
72
- const { extensionId } = this;
73
- const request = await fetch(publishURI({ extensionId, target, deployPercentage }), {
69
+ async publish(publishType = 'DEFAULT_PUBLISH', token = this.fetchToken(), deployPercentage = undefined) {
70
+ const { extensionId, publisherId } = this;
71
+ const body = {
72
+ publishType: this._normalizePublishType(publishType),
73
+ };
74
+ if (deployPercentage !== undefined) {
75
+ body.deployInfos = [{ deployPercentage }];
76
+ }
77
+ const request = await fetch(publishURI(publisherId, extensionId), {
74
78
  method: 'POST',
75
- headers: this._headers(await token),
79
+ headers: {
80
+ ...this._headers(await token),
81
+ 'Content-Type': 'application/json',
82
+ },
83
+ body: JSON.stringify(body),
76
84
  });
77
85
  const response = await request.json();
78
86
  throwIfNotOk(request, response);
79
87
  return response;
80
88
  }
81
- async get(projection = 'DRAFT', token = this.fetchToken()) {
82
- const { extensionId } = this;
83
- const request = await fetch(getURI(extensionId, projection), {
89
+ async setDeployPercentage(deployPercentage, token = this.fetchToken()) {
90
+ const { extensionId, publisherId } = this;
91
+ const request = await fetch(setDeployPercentageURI(publisherId, extensionId), {
92
+ method: 'POST',
93
+ headers: {
94
+ ...this._headers(await token),
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ body: JSON.stringify({ deployPercentage }),
98
+ });
99
+ const response = await request.json();
100
+ throwIfNotOk(request, response);
101
+ }
102
+ async get(token = this.fetchToken()) {
103
+ const { extensionId, publisherId } = this;
104
+ const request = await fetch(fetchStatusURI(publisherId, extensionId), {
84
105
  method: 'GET',
85
106
  headers: this._headers(await token),
86
107
  });
@@ -111,7 +132,7 @@ class APIClient {
111
132
  return response.access_token;
112
133
  }
113
134
  async _waitUploadSuccess(response, maxAwaitInProgressResponseSeconds) {
114
- if (response.uploadState !== 'IN_PROGRESS' || maxAwaitInProgressResponseSeconds < retryIntervalSeconds) {
135
+ if (response.uploadState !== 'UPLOAD_IN_PROGRESS' || maxAwaitInProgressResponseSeconds < retryIntervalSeconds) {
115
136
  return response;
116
137
  }
117
138
  // Wait before checking again
@@ -119,12 +140,34 @@ class APIClient {
119
140
  setTimeout(resolve, retryIntervalSeconds * 1000);
120
141
  });
121
142
  // Retry fetching the item resource
122
- return this._waitUploadSuccess(await this.get('DRAFT'), maxAwaitInProgressResponseSeconds - retryIntervalSeconds);
143
+ const statusResponse = await this.get();
144
+ const retryResponse = {
145
+ crxVersion: response.crxVersion,
146
+ itemId: response.itemId,
147
+ name: response.name,
148
+ uploadState: statusResponse.lastAsyncUploadState,
149
+ };
150
+ return this._waitUploadSuccess(retryResponse, maxAwaitInProgressResponseSeconds - retryIntervalSeconds);
151
+ }
152
+ _normalizePublishType(target) {
153
+ if (target === 'default') {
154
+ return 'DEFAULT_PUBLISH';
155
+ }
156
+ if (target === 'trustedTesters') {
157
+ return 'TRUSTED_TESTERS';
158
+ }
159
+ return target;
123
160
  }
124
161
  _headers(token) {
125
162
  return {
126
163
  Authorization: `Bearer ${token}`,
127
- 'x-goog-api-version': '2',
164
+ };
165
+ }
166
+ _uploadHeaders(token, fileName) {
167
+ return {
168
+ ...this._headers(token),
169
+ 'X-Goog-Upload-Protocol': 'raw',
170
+ 'X-Goog-Upload-File-Name': fileName,
128
171
  };
129
172
  }
130
173
  }
@@ -1,22 +1,28 @@
1
1
  export type APIClientOptions = {
2
2
  extensionId: string;
3
+ publisherId: string;
3
4
  clientId: string;
4
5
  refreshToken: string;
5
6
  clientSecret: string | undefined;
6
7
  };
7
8
  export type ItemResource = {
8
- kind: 'chromewebstore#item';
9
- id: string;
10
- publicKey: string;
11
- uploadState: 'FAILURE' | 'IN_PROGRESS' | 'NOT_FOUND' | 'SUCCESS';
12
- itemError: Array<{
13
- error_code: string;
14
- error_detail: string;
15
- }>;
9
+ crxVersion: string;
10
+ itemId: string;
11
+ name: string;
12
+ uploadState: string;
16
13
  };
14
+ export type PublishType = 'PUBLISH_TYPE_UNSPECIFIED' | 'DEFAULT_PUBLISH' | 'TRUSTED_TESTERS' | 'STAGED_PUBLISH';
17
15
  export type PublishResponse = {
18
- kind: 'chromewebstore#item';
19
- item_id: string;
20
- status: Array<'OK' | 'NOT_AUTHORIZED' | 'INVALID_DEVELOPER' | 'DEVELOPER_NO_OWNERSHIP' | 'DEVELOPER_SUSPENDED' | 'ITEM_NOT_FOUND' | 'ITEM_PENDING_REVIEW' | 'ITEM_TAKEN_DOWN' | 'PUBLISHER_SUSPENDED'>;
21
- statusDetail: string[];
16
+ itemId: string;
17
+ name: string;
18
+ state: string;
19
+ };
20
+ export type ItemStatusResponse = {
21
+ itemId: string;
22
+ lastAsyncUploadState: string;
23
+ name: string;
24
+ publicKey: string;
25
+ publishedItemRevisionStatus: unknown;
26
+ submittedItemRevisionStatus: unknown;
27
+ takenDown: boolean;
22
28
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-webstore-upload",
3
- "version": "4.0.2",
3
+ "version": "5.0.0-0",
4
4
  "description": "Upload Chrome Extensions to the Chrome Web Store",
5
5
  "keywords": [
6
6
  "chrome",
@@ -34,7 +34,7 @@
34
34
  "prepack": "npm run build",
35
35
  "test": "xo && vitest run && tsc",
36
36
  "test:upload": "eval $(cat .env) node test/live-test.js",
37
- "upload": "npm run bundle && npm run test:upload"
37
+ "upload": "npm run build && npm run bundle && npm run test:upload"
38
38
  },
39
39
  "xo": {
40
40
  "rules": {
package/readme.md CHANGED
@@ -14,6 +14,8 @@ npm install --save-dev chrome-webstore-upload
14
14
 
15
15
  You will need a Google API `clientId`, `clientSecret` and `refreshToken`. Use [the guide]( https://github.com/fregante/chrome-webstore-upload-keys).
16
16
 
17
+ You also need your Chrome Web Store `publisherId` (your developer account identifier, not the extension ID). You can find it in the Chrome Web Store Developer Dashboard URL when logged in.
18
+
17
19
  ## Usage
18
20
 
19
21
  All methods return a promise.
@@ -25,6 +27,7 @@ import chromeWebstoreUpload from 'chrome-webstore-upload';
25
27
 
26
28
  const store = chromeWebstoreUpload({
27
29
  extensionId: 'ecnglinljpjkbgmdpeiglonddahpbkeb',
30
+ publisherId: 'your-publisher-id',
28
31
  clientId: 'xxxxxxxxxx',
29
32
  clientSecret: 'xxxxxxxxxx',
30
33
  refreshToken: 'xxxxxxxxxx',
@@ -33,7 +36,7 @@ const store = chromeWebstoreUpload({
33
36
 
34
37
  ### Upload to existing extension
35
38
 
36
- You can upload a zip file, crx file, or a directory. If you provide a directory, it will be automatically zipped.
39
+ You can upload a zip file, crx file, or a directory. If you provide a directory, it will be automatically zipped. Crx files are only supported as path, not as stream.
37
40
 
38
41
  ```javascript
39
42
  import fs from 'fs';
@@ -44,7 +47,7 @@ const token = 'xxxx'; // optional. One will be fetched if not provided
44
47
  const maxAwaitInProgressResponseSeconds = 60; // optional. If the API response is IN_PROGRESS, this method will wait until it becomes successful, or until the specified timeout
45
48
  const response = await store.uploadExisting(myZipFile, token, maxAwaitInProgressResponseSeconds);
46
49
  // response is a Resource Representation
47
- // https://developer.chrome.com/webstore/webstore_api/items#resource
50
+ // https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/upload
48
51
  ```
49
52
 
50
53
  ```javascript
@@ -63,22 +66,32 @@ const response = await store.uploadExisting('./path/to/extension.crx', token, ma
63
66
  ### Publish extension
64
67
 
65
68
  ```javascript
66
- const target = 'default'; // optional. Can also be 'trustedTesters'
69
+ const publishType = 'DEFAULT_PUBLISH'; // optional. Can also be 'TRUSTED_TESTERS' or 'STAGED_PUBLISH'
67
70
  const token = 'xxxx'; // optional. One will be fetched if not provided
68
- const deployPercentage = 25; // optional. Will default to 100%.
69
- const response = await store.publish(target, token, deployPercentage);
71
+ const deployPercentage = 25; // optional. Sets the initial rollout percentage.
72
+ const response = await store.publish(publishType, token, deployPercentage);
70
73
  // response is documented here:
71
- // https://developer.chrome.com/webstore/webstore_api/items#publish
74
+ // https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/publish
75
+ ```
76
+
77
+ ### Set deployment rollout percentage
78
+
79
+ Update the deployment percentage for an already-published extension without triggering a re-review:
80
+
81
+ ```javascript
82
+ const deployPercentage = 50; // required. Must be higher than the current value.
83
+ const token = 'xxxx'; // optional. One will be fetched if not provided
84
+ await store.setDeployPercentage(deployPercentage, token);
85
+ // https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/setPublishedDeployPercentage
72
86
  ```
73
87
 
74
- ### Get a Chrome Web Store item
88
+ ### Get Chrome Web Store item status
75
89
 
76
90
  ```javascript
77
- const projection = "DRAFT"; // optional. Can also be 'PUBLISHED' but only "DRAFT" is supported at this time.
78
91
  const token = "xxxx"; // optional. One will be fetched if not provided
79
- const response = await store.get(projection, token);
92
+ const response = await store.get(token);
80
93
  // response is documented here:
81
- // https://developer.chrome.com/docs/webstore/webstore_api/items#get
94
+ // https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/fetchStatus
82
95
  ```
83
96
 
84
97
  ### Fetch token