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.
- package/distribution/errors.js +1 -9
- package/distribution/index.d.ts +12 -5
- package/distribution/index.js +69 -26
- package/distribution/types.d.ts +18 -12
- package/package.json +2 -2
- package/readme.md +23 -10
package/distribution/errors.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/distribution/index.d.ts
CHANGED
|
@@ -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(
|
|
14
|
-
|
|
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
|
-
|
|
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;
|
package/distribution/index.js
CHANGED
|
@@ -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://
|
|
7
|
+
const rootURI = 'https://chromewebstore.googleapis.com';
|
|
8
8
|
export const refreshTokenURI = 'https://www.googleapis.com/oauth2/v4/token';
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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: '
|
|
61
|
-
headers: this.
|
|
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(
|
|
72
|
-
const { extensionId } = this;
|
|
73
|
-
const
|
|
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:
|
|
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
|
|
82
|
-
const { extensionId } = this;
|
|
83
|
-
const request = await fetch(
|
|
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 !== '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/distribution/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
uploadState:
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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": "
|
|
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/
|
|
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
|
|
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.
|
|
69
|
-
const response = await store.publish(
|
|
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/
|
|
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
|
|
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(
|
|
92
|
+
const response = await store.get(token);
|
|
80
93
|
// response is documented here:
|
|
81
|
-
// https://developer.chrome.com/docs/webstore/
|
|
94
|
+
// https://developer.chrome.com/docs/webstore/api/reference/rest/v2/publishers.items/fetchStatus
|
|
82
95
|
```
|
|
83
96
|
|
|
84
97
|
### Fetch token
|