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.
- package/distribution/errors.js +1 -9
- package/distribution/index.d.ts +7 -6
- package/distribution/index.js +60 -25
- package/distribution/types.d.ts +18 -12
- package/distribution/zip-dir.js +5 -3
- package/package.json +1 -1
- package/readme.md +22 -9
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,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(
|
|
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
|
-
'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
|
};
|
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;
|
|
@@ -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: '
|
|
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(
|
|
73
|
-
const { extensionId } = this;
|
|
74
|
-
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), {
|
|
75
78
|
method: 'POST',
|
|
76
|
-
headers:
|
|
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
|
|
83
|
-
const { extensionId } = this;
|
|
84
|
-
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), {
|
|
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 !== '
|
|
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
|
-
|
|
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) {
|
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/distribution/zip-dir.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
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
|
|
7
|
-
const files =
|
|
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
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/
|
|
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
|