chrome-webstore-upload 4.0.3 → 5.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,26 +1,27 @@
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;
20
22
  };
21
23
  _uploadHeaders(token: string, fileName: string): {
22
24
  Authorization: string;
23
- 'x-goog-api-version': string;
24
25
  'X-Goog-Upload-Protocol': string;
25
26
  'X-Goog-Upload-File-Name': string;
26
27
  };
@@ -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;
@@ -56,9 +53,9 @@ class APIClient {
56
53
  const readStream = typeof streamOrPath === 'string'
57
54
  ? await getStreamFromPath(streamOrPath)
58
55
  : streamOrPath;
59
- const { extensionId } = this;
60
- const request = await fetch(uploadExistingURI(extensionId), {
61
- method: 'PUT',
56
+ const { extensionId, publisherId } = this;
57
+ const request = await fetch(uploadExistingURI(publisherId, extensionId), {
58
+ method: 'POST',
62
59
  headers: this._uploadHeaders(await token, fileName),
63
60
  // @ts-expect-error Node extension? 🤷‍♂️ Required https://github.com/nodejs/node/issues/46221
64
61
  duplex: 'half',
@@ -69,19 +66,42 @@ class APIClient {
69
66
  throwIfNotOk(request, response);
70
67
  return this._waitUploadSuccess(response, maxAwaitInProgressResponseSeconds);
71
68
  }
72
- async publish(target = 'default', token = this.fetchToken(), deployPercentage = undefined) {
73
- const { extensionId } = this;
74
- 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), {
75
78
  method: 'POST',
76
- headers: this._headers(await token),
79
+ headers: {
80
+ ...this._headers(await token),
81
+ 'Content-Type': 'application/json',
82
+ },
83
+ body: JSON.stringify(body),
77
84
  });
78
85
  const response = await request.json();
79
86
  throwIfNotOk(request, response);
80
87
  return response;
81
88
  }
82
- async get(projection = 'DRAFT', token = this.fetchToken()) {
83
- const { extensionId } = this;
84
- 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), {
85
105
  method: 'GET',
86
106
  headers: this._headers(await token),
87
107
  });
@@ -112,7 +132,7 @@ class APIClient {
112
132
  return response.access_token;
113
133
  }
114
134
  async _waitUploadSuccess(response, maxAwaitInProgressResponseSeconds) {
115
- if (response.uploadState !== 'IN_PROGRESS' || maxAwaitInProgressResponseSeconds < retryIntervalSeconds) {
135
+ if (response.uploadState !== 'UPLOAD_IN_PROGRESS' || maxAwaitInProgressResponseSeconds < retryIntervalSeconds) {
116
136
  return response;
117
137
  }
118
138
  // Wait before checking again
@@ -120,12 +140,27 @@ class APIClient {
120
140
  setTimeout(resolve, retryIntervalSeconds * 1000);
121
141
  });
122
142
  // Retry fetching the item resource
123
- 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;
124
160
  }
125
161
  _headers(token) {
126
162
  return {
127
163
  Authorization: `Bearer ${token}`,
128
- 'x-goog-api-version': '2',
129
164
  };
130
165
  }
131
166
  _uploadHeaders(token, fileName) {
@@ -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
  };
@@ -1,10 +1,12 @@
1
1
  import { readdir } from 'node:fs/promises';
2
- import { basename, join } from 'node:path';
2
+ import { join, relative } from 'node:path';
3
3
  import { isNotJunk } from 'junk';
4
4
  import yazl from 'yazl';
5
5
  export default async function zipStreamFromDirectory(directory) {
6
- const allFiles = await readdir(directory, { recursive: true });
7
- const files = allFiles.filter(file => isNotJunk(basename(file)));
6
+ const entries = await readdir(directory, { recursive: true, withFileTypes: true });
7
+ const files = entries
8
+ .filter(entry => entry.isFile() && isNotJunk(entry.name))
9
+ .map(entry => relative(directory, join(entry.parentPath, entry.name)));
8
10
  if (!files.includes('manifest.json')) {
9
11
  throw new Error(`manifest.json was not found in ${directory}`);
10
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-webstore-upload",
3
- "version": "4.0.3",
3
+ "version": "5.0.0",
4
4
  "description": "Upload Chrome Extensions to the Chrome Web Store",
5
5
  "keywords": [
6
6
  "chrome",
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',
@@ -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