gutterpress 0.10.4 → 0.10.5-beta.1
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/README.md +7 -3
- package/dist/api/index.d.ts +7 -3
- package/dist/api/index.js +24 -2
- package/dist/{audit-6gsbjcyp.js → audit-9pf96y1w.js} +4 -4
- package/dist/{build-8gx7gkgw.js → build-jw9kynts.js} +4 -4
- package/dist/{cli-7hay1r0h.js → cli-0t4zfevc.js} +1 -1
- package/dist/{cli-zddg7r27.js → cli-5w9y6r2f.js} +928 -74
- package/dist/{cli-h617210r.js → cli-nb902265.js} +1 -1
- package/dist/{cli-d613m9n5.js → cli-xewqry0j.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{doctor-mrqpw7g3.js → doctor-6vmxh7mz.js} +2 -2
- package/dist/{engine-dzrzyyew.js → engine-fwe8djyn.js} +1 -1
- package/dist/{engine-pta3yeyt.js → engine-mtyjc4v6.js} +2 -2
- package/dist/{index-1z8a090c.js → index-3xv7vwv2.js} +972 -120
- package/dist/{index-cm4dywtk.js → index-ja0p4w5f.js} +1 -1
- package/dist/{index-p4yq14qw.js → index-k5hcp8wj.js} +1 -1
- package/dist/index.js +25 -3
- package/dist/lib/markdown/gutterpress-css.d.ts +10 -2
- package/dist/lib/open-path.d.ts +27 -0
- package/dist/lib/publish/connect-google.d.ts +34 -0
- package/dist/lib/publish/connect.d.ts +23 -0
- package/dist/lib/publish/google-auth.d.ts +104 -0
- package/dist/lib/publish/google-drive.d.ts +88 -0
- package/dist/lib/publish/google-errors.d.ts +49 -0
- package/dist/lib/publish/providers/gdrive.d.ts +25 -0
- package/dist/lib/publish/run-publish.d.ts +14 -1
- package/dist/lib/publish/types.d.ts +46 -2
- package/dist/lib/remote-auth/token-store.d.ts +1 -1
- package/dist/{lint-gmymvnm3.js → lint-kzvbrn3d.js} +4 -4
- package/dist/{manifest.schema-084rxtwp.json → manifest.schema-hn6ac0ae.json} +25 -0
- package/dist/{new-hw7s7jne.js → new-cyna8tyn.js} +4 -4
- package/dist/{plugin-0qtfgjr2.js → plugin-mpc5v8sr.js} +4 -4
- package/dist/{preflight-j99dpj5e.js → preflight-fc178as2.js} +4 -4
- package/dist/{preview-sc3rcrkh.js → preview-qnbbtyqb.js} +4 -4
- package/dist/{publish-xwrwvrh0.js → publish-gmgpyh4n.js} +58 -9
- package/dist/schema/manifest.types.d.ts +20 -0
- package/dist/{validate-evzfg9m6.js → validate-0t3vjpmk.js} +4 -4
- package/package.json +1 -1
|
@@ -593,7 +593,7 @@ import { fileURLToPath } from "node:url";
|
|
|
593
593
|
var favicon_default = "./favicon-wkbm9cjn.ico";
|
|
594
594
|
|
|
595
595
|
// src/assets/manifest.schema.json
|
|
596
|
-
var manifest_schema_default = "./manifest.schema-
|
|
596
|
+
var manifest_schema_default = "./manifest.schema-hn6ac0ae.json";
|
|
597
597
|
|
|
598
598
|
// src/assets/preview/scripts/preview-interface.js
|
|
599
599
|
var preview_interface_default = "./preview-interface-9rwz21yw.js";
|
package/dist/index.js
CHANGED
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
BUILT_IN_TEMPLATE_IDS,
|
|
10
10
|
BUILT_IN_THEME_IDS,
|
|
11
11
|
FileTokenStore,
|
|
12
|
+
GDRIVE_HOST,
|
|
12
13
|
GITHUB_HOST,
|
|
14
|
+
GOOGLE_NOT_CONFIGURED_MESSAGE,
|
|
13
15
|
GitHubAuthProvider,
|
|
16
|
+
GoogleAuthProvider,
|
|
14
17
|
MANIFEST_FILENAMES,
|
|
15
18
|
PLUGINS_DIR,
|
|
16
19
|
PRESETS,
|
|
@@ -32,10 +35,12 @@ import {
|
|
|
32
35
|
checkToolAvailability,
|
|
33
36
|
cloneRepository,
|
|
34
37
|
connectGenericHost,
|
|
38
|
+
connectGoogleDrive,
|
|
35
39
|
connectPublishProvider,
|
|
36
40
|
defaultConfigDir,
|
|
37
41
|
deleteSnippet,
|
|
38
42
|
diagnoseProjectRemote,
|
|
43
|
+
disconnectPublishCredential,
|
|
39
44
|
escapeYamlScalar,
|
|
40
45
|
executeAndReport,
|
|
41
46
|
executeValidation,
|
|
@@ -74,6 +79,7 @@ import {
|
|
|
74
79
|
log,
|
|
75
80
|
openPath,
|
|
76
81
|
parseRemoteOrigin,
|
|
82
|
+
pkceChallengeFromVerifier,
|
|
77
83
|
publishConnectionStatus,
|
|
78
84
|
publishCredentialKey,
|
|
79
85
|
publishProviderFor,
|
|
@@ -84,13 +90,18 @@ import {
|
|
|
84
90
|
readThemeCss,
|
|
85
91
|
removeProjectTheme,
|
|
86
92
|
reportMissingTools,
|
|
93
|
+
requireGoogleClientCredentials,
|
|
87
94
|
resolveActiveStyles,
|
|
88
95
|
resolveBuiltInTheme,
|
|
89
96
|
resolveCheckSelectors,
|
|
90
97
|
resolveConfig,
|
|
91
98
|
resolveGitHubClientId,
|
|
99
|
+
resolveGoogleClientId,
|
|
100
|
+
resolveGoogleClientSecret,
|
|
101
|
+
resolvePublishFormat,
|
|
92
102
|
resolvePublishRequest,
|
|
93
103
|
revertTheme,
|
|
104
|
+
revokeGoogleCredential,
|
|
94
105
|
ruleRemoteUrls,
|
|
95
106
|
ruleRiskyProps,
|
|
96
107
|
ruleSyntax,
|
|
@@ -113,15 +124,15 @@ import {
|
|
|
113
124
|
syncProject,
|
|
114
125
|
testRemoteAccess,
|
|
115
126
|
validateProjectPlugins
|
|
116
|
-
} from "./index-
|
|
127
|
+
} from "./index-3xv7vwv2.js";
|
|
117
128
|
import {
|
|
118
129
|
BUILD_DIAGNOSTIC_CODES
|
|
119
|
-
} from "./index-
|
|
130
|
+
} from "./index-ja0p4w5f.js";
|
|
120
131
|
import {
|
|
121
132
|
BuildError,
|
|
122
133
|
DEFAULT_PRINT_OPTS,
|
|
123
134
|
readyProbeExpr
|
|
124
|
-
} from "./index-
|
|
135
|
+
} from "./index-k5hcp8wj.js";
|
|
125
136
|
import {
|
|
126
137
|
AUTO_SNAPSHOT_MESSAGE,
|
|
127
138
|
HISTORY_PAGE_LIMIT,
|
|
@@ -162,15 +173,20 @@ export {
|
|
|
162
173
|
ruleSyntax,
|
|
163
174
|
ruleRiskyProps,
|
|
164
175
|
ruleRemoteUrls,
|
|
176
|
+
revokeGoogleCredential,
|
|
165
177
|
revertTheme,
|
|
166
178
|
restoreVersionWithBackup,
|
|
167
179
|
resolvePublishRequest,
|
|
180
|
+
resolvePublishFormat,
|
|
168
181
|
resolveLogger,
|
|
182
|
+
resolveGoogleClientSecret,
|
|
183
|
+
resolveGoogleClientId,
|
|
169
184
|
resolveGitHubClientId,
|
|
170
185
|
resolveConfig,
|
|
171
186
|
resolveCheckSelectors,
|
|
172
187
|
resolveBuiltInTheme,
|
|
173
188
|
resolveActiveStyles,
|
|
189
|
+
requireGoogleClientCredentials,
|
|
174
190
|
reportMissingTools,
|
|
175
191
|
repoSubPath,
|
|
176
192
|
repoRootForSource,
|
|
@@ -185,6 +201,7 @@ export {
|
|
|
185
201
|
publishCredentialKey,
|
|
186
202
|
publishConnectionStatus,
|
|
187
203
|
providerFor,
|
|
204
|
+
pkceChallengeFromVerifier,
|
|
188
205
|
parseRemoteOrigin,
|
|
189
206
|
openPath,
|
|
190
207
|
log,
|
|
@@ -225,11 +242,13 @@ export {
|
|
|
225
242
|
executeValidation,
|
|
226
243
|
executeAndReport,
|
|
227
244
|
escapeYamlScalar,
|
|
245
|
+
disconnectPublishCredential,
|
|
228
246
|
diagnoseProjectRemote,
|
|
229
247
|
detectProjectSource,
|
|
230
248
|
deleteSnippet,
|
|
231
249
|
defaultConfigDir,
|
|
232
250
|
connectPublishProvider,
|
|
251
|
+
connectGoogleDrive,
|
|
233
252
|
connectGenericHost,
|
|
234
253
|
cloneRepository,
|
|
235
254
|
checkToolAvailability,
|
|
@@ -254,8 +273,11 @@ export {
|
|
|
254
273
|
PLUGINS_DIR,
|
|
255
274
|
MANIFEST_FILENAMES,
|
|
256
275
|
HISTORY_PAGE_LIMIT,
|
|
276
|
+
GoogleAuthProvider,
|
|
257
277
|
GitHubAuthProvider,
|
|
278
|
+
GOOGLE_NOT_CONFIGURED_MESSAGE,
|
|
258
279
|
GITHUB_HOST,
|
|
280
|
+
GDRIVE_HOST,
|
|
259
281
|
FileTokenStore,
|
|
260
282
|
DEFAULT_PRINT_OPTS,
|
|
261
283
|
BuildError,
|
|
@@ -38,8 +38,16 @@
|
|
|
38
38
|
* (shape-outside; the pipeline mirrors the src into
|
|
39
39
|
* --gp-shape because CSS url() contexts can't read
|
|
40
40
|
* attr()). Floats only; inert elsewhere.
|
|
41
|
-
* .gp-pin — pins within
|
|
42
|
-
*
|
|
41
|
+
* .gp-pin — pins within its nearest POSITIONED ANCESTOR, falling
|
|
42
|
+
* back to the page/spread. Core gives `.page`/`.spread`
|
|
43
|
+
* `position: relative` (markers.js MARKER_CSS), so with no
|
|
44
|
+
* other positioned ancestor in between that is the page —
|
|
45
|
+
* the common case. A theme that positions something closer
|
|
46
|
+
* (a `.section` card, a component shell) becomes the pin's
|
|
47
|
+
* frame instead, BY DESIGN: that is how a pin is scoped to
|
|
48
|
+
* a card rather than a sheet. Page-level furniture must
|
|
49
|
+
* therefore be authored outside such a wrapper.
|
|
50
|
+
* Centered on both axes unless combined with the edge
|
|
43
51
|
* modifiers .gp-top/.gp-bottom/.gp-left/.gp-right.
|
|
44
52
|
* .gp-flush — with .gp-pin + an edge, the art sits on the PAPER's
|
|
45
53
|
* edge rather than on the text block's. No CSS rule here:
|
package/dist/lib/open-path.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
export interface OpenPathSpawnSpec {
|
|
2
|
+
cmd: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
options: {
|
|
5
|
+
detached: boolean;
|
|
6
|
+
stdio: "ignore";
|
|
7
|
+
windowsVerbatimArguments?: boolean;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build the {cmd, args, options} triple openPath() spawns, without actually
|
|
12
|
+
* spawning anything — kept separate so the win32 quoting can be unit tested
|
|
13
|
+
* on non-Windows CI/dev machines.
|
|
14
|
+
*
|
|
15
|
+
* win32 note: Node's default argv-to-command-line quoting (libuv) only
|
|
16
|
+
* quotes an argument if it contains a space/tab/quote character. A bare
|
|
17
|
+
* URL like a Google OAuth auth link has no spaces (everything is
|
|
18
|
+
* percent-encoded) but does contain unescaped "&" between query params, so
|
|
19
|
+
* libuv would pass it UNQUOTED. cmd.exe's own parser then treats the first
|
|
20
|
+
* unescaped "&" as a command separator, truncating the URL. The fix is the
|
|
21
|
+
* well-established Node technique for this cmd.exe/start quirk: pass
|
|
22
|
+
* `windowsVerbatimArguments: true` (which disables Node's own quoting
|
|
23
|
+
* entirely) and do the quoting ourselves — an empty quoted string for the
|
|
24
|
+
* `start` window-title placeholder, and the target wrapped in quotes so
|
|
25
|
+
* `start` doesn't mistake an unquoted first token for the title.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildOpenPathSpawnSpec(filePath: string, platform?: NodeJS.Platform): OpenPathSpawnSpec;
|
|
1
28
|
/**
|
|
2
29
|
* Open a file in the user's default OS desktop. Detached + unref'd so the
|
|
3
30
|
* caller can exit immediately without waiting on the desktop process.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Google Drive connect flow (#221) — the ONE implementation behind
|
|
3
|
+
* both the CLI's `--connect` branch and (Phase 2) the desktop's connect
|
|
4
|
+
* route, mirroring how `connect.ts` is shared for pasted-token providers.
|
|
5
|
+
*
|
|
6
|
+
* Unlike `connectPublishProvider` (verify-before-store for a PASTED token),
|
|
7
|
+
* this flow is verify-by-construction: `GoogleAuthProvider.connect()` only
|
|
8
|
+
* returns a credential after a successful token exchange, so there is no
|
|
9
|
+
* separate "try it against the platform" step — storing it IS the proof.
|
|
10
|
+
*/
|
|
11
|
+
import { type GoogleHostCallbacks } from "./google-auth.ts";
|
|
12
|
+
import { type PublishDeps } from "./types.ts";
|
|
13
|
+
export interface ConnectGoogleDriveOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Optional account label for a NAMED credential (two Google accounts) —
|
|
16
|
+
* stored under the compound `gdrive#<account>` key, same mechanism as
|
|
17
|
+
* every other provider's named accounts (types.ts `publishCredentialKey`).
|
|
18
|
+
*/
|
|
19
|
+
account?: string;
|
|
20
|
+
/** Explicit client id/secret overrides (tests; advanced use). */
|
|
21
|
+
clientId?: string;
|
|
22
|
+
clientSecret?: string;
|
|
23
|
+
/** Override the system-browser opener (tests; advanced use). */
|
|
24
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface ConnectGoogleDriveResult {
|
|
27
|
+
connected: true;
|
|
28
|
+
/** The connected account's email, when Google returned one. */
|
|
29
|
+
email?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run the loopback+PKCE flow and store the resulting refresh-token credential.
|
|
33
|
+
*/
|
|
34
|
+
export declare function connectGoogleDrive(options: ConnectGoogleDriveOptions, deps: PublishDeps, callbacks: GoogleHostCallbacks): Promise<ConnectGoogleDriveResult>;
|
|
@@ -21,3 +21,26 @@ export declare function connectPublishProvider(options: ConnectPublishProviderOp
|
|
|
21
21
|
connected: true;
|
|
22
22
|
providerId: string;
|
|
23
23
|
}>;
|
|
24
|
+
export interface DisconnectPublishCredentialOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Await the best-effort revoke before returning. The CLI passes `true` (a
|
|
27
|
+
* one-shot process can afford to wait before printing success); the
|
|
28
|
+
* desktop's disconnect routes leave this `false` (the default) so "Remove
|
|
29
|
+
* this key" resolves immediately even offline, with the revoke — which
|
|
30
|
+
* carries its own ~10s network timeout — running in the background.
|
|
31
|
+
*/
|
|
32
|
+
awaitRevoke?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Delete a stored credential by its TokenStore key, one implementation
|
|
36
|
+
* shared by every disconnect entry point (the CLI's `--disconnect`, and the
|
|
37
|
+
* desktop's publish:disconnect + remote:disconnectHost routes — this key
|
|
38
|
+
* shape covers both a publish provider's compound `<host>#<account>` key and
|
|
39
|
+
* a plain remote-host key). The local delete always happens; before it, when
|
|
40
|
+
* the stored credential's `kind` supports a provider-side revoke (today only
|
|
41
|
+
* `google-oauth`, via `revokeGoogleCredential` — the same slot a future
|
|
42
|
+
* revocable oauth provider would use), a best-effort revoke is attempted.
|
|
43
|
+
* `revokeGoogleCredential` never throws, so a failed revoke can never block
|
|
44
|
+
* or fail the local delete.
|
|
45
|
+
*/
|
|
46
|
+
export declare function disconnectPublishCredential(key: string, deps: Pick<PublishDeps, "tokenStore" | "fetch">, options?: DisconnectPublishCredentialOptions): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { HostCredential } from "../remote-auth/token-store.ts";
|
|
2
|
+
/** Logical TokenStore host key for the stored refresh-token credential. */
|
|
3
|
+
export declare const GDRIVE_HOST = "gdrive";
|
|
4
|
+
export declare const GOOGLE_REVOKE_ENDPOINT = "https://oauth2.googleapis.com/revoke";
|
|
5
|
+
export declare const OFFLINE_MESSAGE = "Couldn't reach Google. Check your connection and try again.";
|
|
6
|
+
/** Client id resolution — see {@link resolveClientValue}. */
|
|
7
|
+
export declare function resolveGoogleClientId(explicit?: string): string;
|
|
8
|
+
/** Client secret resolution — see {@link resolveClientValue}, and the module
|
|
9
|
+
* header re: why a Google installed-app "secret" is safe to embed as a
|
|
10
|
+
* default (ADR 0011) — this is deliberately NOT the GitHub rule. */
|
|
11
|
+
export declare function resolveGoogleClientSecret(explicit?: string): string;
|
|
12
|
+
/** Friendly, load-bearing product decision (see task brief): fail EARLY and
|
|
13
|
+
* clearly when this build has no Google OAuth client configured, rather than
|
|
14
|
+
* starting a loopback listener that can never complete an exchange. */
|
|
15
|
+
export declare const GOOGLE_NOT_CONFIGURED_MESSAGE = "Google Drive publishing isn't configured on this build yet. Set GUTTERPRESS_GOOGLE_CLIENT_ID and GUTTERPRESS_GOOGLE_CLIENT_SECRET to enable it.";
|
|
16
|
+
/** Resolve + require both Google OAuth client credentials, or throw the
|
|
17
|
+
* friendly not-configured error. Shared by {@link GoogleAuthProvider.connect}
|
|
18
|
+
* and the Drive client's token refresh (both need id+secret). */
|
|
19
|
+
export declare function requireGoogleClientCredentials(clientIdOpt?: string, clientSecretOpt?: string): {
|
|
20
|
+
clientId: string;
|
|
21
|
+
clientSecret: string;
|
|
22
|
+
};
|
|
23
|
+
/** D4: a revoked/expired refresh token maps to one friendly reconnect message. */
|
|
24
|
+
export declare const RECONNECT_MESSAGE = "Your Google Drive connection expired or was revoked. Connect Google Drive again.";
|
|
25
|
+
/** The one scope the provider needs — and the only one it requests (D1). */
|
|
26
|
+
export declare const DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
27
|
+
/**
|
|
28
|
+
* Backstop for a token issued without the Drive scope. With a single scope
|
|
29
|
+
* requested (see GOOGLE_OAUTH_SCOPE) Google's consent screen has nothing to
|
|
30
|
+
* partially grant, so this should not happen — but Google's own guidance is
|
|
31
|
+
* to read the token response's `scope` rather than assume, and a sign-in
|
|
32
|
+
* that "succeeds" without Drive access would otherwise surface only as a
|
|
33
|
+
* bare 403 on the author's first publish.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DRIVE_PERMISSION_NOT_GRANTED_MESSAGE = "Google sign-in finished, but it didn't include the Google Drive permission, so Gutterpress can't create or see any files. Connect Google Drive again and allow it.";
|
|
36
|
+
/** Whether a token response's granted `scope` list includes `wanted`. An
|
|
37
|
+
* ABSENT field is treated as unknown (true): Google always sends it, and if
|
|
38
|
+
* it ever didn't, the about.get call right after connect still catches an
|
|
39
|
+
* insufficient grant with Google's own reason (see `fetchEmail`). */
|
|
40
|
+
export declare function grantedScopesInclude(scope: string | undefined, wanted: string): boolean;
|
|
41
|
+
/** Host-supplied callbacks for the interactive connect flow. There is no
|
|
42
|
+
* "user code" to display (unlike the GitHub device flow) — just the URL the
|
|
43
|
+
* browser was (or should be) sent to, and an optional cancellation signal. */
|
|
44
|
+
export interface GoogleHostCallbacks {
|
|
45
|
+
/** Called once with the auth URL, so the host UI can show a "browser
|
|
46
|
+
* didn't open? click here" fallback link while waiting. */
|
|
47
|
+
onAuthUrl(url: string): void;
|
|
48
|
+
/** Optional cancellation (user closed the dialog / pressed Ctrl+C). */
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
export interface GoogleAuthProviderOptions {
|
|
52
|
+
/** Explicit client id (overrides env + default). */
|
|
53
|
+
clientId?: string;
|
|
54
|
+
/** Explicit client secret (overrides env + default). */
|
|
55
|
+
clientSecret?: string;
|
|
56
|
+
/** Injectable fetch for tests. Defaults to global fetch. */
|
|
57
|
+
fetchImpl?: typeof fetch;
|
|
58
|
+
/** Opens the auth URL in the user's default browser. Defaults to the CLI's
|
|
59
|
+
* `openPath()` (system browser opener). Tests inject a no-op so they never
|
|
60
|
+
* spawn a real browser process. Failure to open is non-fatal — `onAuthUrl`
|
|
61
|
+
* already handed the host UI the URL as a fallback link. */
|
|
62
|
+
openBrowser?: (url: string) => Promise<void>;
|
|
63
|
+
/** Overall deadline (ms) waiting for the browser redirect. Default 5 min. */
|
|
64
|
+
timeoutMs?: number;
|
|
65
|
+
/** Test-only: bind the loopback listener to this exact port instead of an
|
|
66
|
+
* OS-assigned ephemeral one (0). Lets tests deterministically force a bind
|
|
67
|
+
* failure (EADDRINUSE) by pre-binding the port themselves. Never set this
|
|
68
|
+
* in production — an OS-assigned port is what makes the loopback flow work
|
|
69
|
+
* without a fixed-port conflict. */
|
|
70
|
+
port?: number;
|
|
71
|
+
}
|
|
72
|
+
/** PKCE S256 challenge derivation — exported as a pure helper so tests can
|
|
73
|
+
* verify the verifier/challenge relationship independently of the flow. */
|
|
74
|
+
export declare function pkceChallengeFromVerifier(verifier: string): string;
|
|
75
|
+
/** Google OAuth loopback+PKCE credential acquisition (D2/D3). */
|
|
76
|
+
export declare class GoogleAuthProvider {
|
|
77
|
+
private readonly clientIdOpt;
|
|
78
|
+
private readonly clientSecretOpt;
|
|
79
|
+
private readonly fetchImpl;
|
|
80
|
+
private readonly openBrowser;
|
|
81
|
+
private readonly timeoutMs;
|
|
82
|
+
private readonly bindPort;
|
|
83
|
+
constructor(options?: GoogleAuthProviderOptions);
|
|
84
|
+
connect(callbacks: GoogleHostCallbacks): Promise<HostCredential>;
|
|
85
|
+
/**
|
|
86
|
+
* Email lookup after auth, via Drive's `about.get` — which doubles as the
|
|
87
|
+
* first real Drive API call the new token makes. A 401/403 answer here is
|
|
88
|
+
* NOT best-effort: it means the token can't use the Drive API at all (the
|
|
89
|
+
* common case is `accessNotConfigured` — the Drive API isn't enabled for
|
|
90
|
+
* the OAuth client's Cloud project, ADR 0011), so storing the credential
|
|
91
|
+
* would only defer the failure to the author's first publish, as a bare
|
|
92
|
+
* "HTTP 403" with nothing to act on — exactly the trap the 0.10.5 bring-up
|
|
93
|
+
* hit. Fail connect right here, with Google's reason, and store nothing.
|
|
94
|
+
* Everything else (a network blip, a 5xx, a body without the field) stays
|
|
95
|
+
* non-fatal, mirroring GitHubAuthProvider's fetchUsername precedent: the
|
|
96
|
+
* email is only a label.
|
|
97
|
+
*/
|
|
98
|
+
private fetchEmail;
|
|
99
|
+
}
|
|
100
|
+
/** Best-effort revoke at Google (used by disconnect, D4/D6). Never throws —
|
|
101
|
+
* a failed revoke must not block the local credential from being deleted. */
|
|
102
|
+
export declare function revokeGoogleCredential(refreshToken: string, options?: {
|
|
103
|
+
fetchImpl?: typeof fetch;
|
|
104
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export declare const OFFLINE_MESSAGE = "Couldn't reach Google Drive. Check your connection and try again.";
|
|
2
|
+
/**
|
|
3
|
+
* Escape a value for safe interpolation into a Drive API `q` search string.
|
|
4
|
+
* Drive's query grammar uses single-quoted string literals with `\'` and
|
|
5
|
+
* `\\` as the only escapes — an unescaped `'` in a folder/file name would
|
|
6
|
+
* otherwise terminate the literal early and let the rest of the name be
|
|
7
|
+
* interpreted as query syntax.
|
|
8
|
+
*/
|
|
9
|
+
export declare function escapeDriveQueryValue(value: string): string;
|
|
10
|
+
export interface RefreshedToken {
|
|
11
|
+
accessToken: string;
|
|
12
|
+
expiresIn: number;
|
|
13
|
+
}
|
|
14
|
+
/** Mint a fresh access token from the stored refresh token. Maps Google's
|
|
15
|
+
* `invalid_grant` to the D4 reconnect message; never logs the token values. */
|
|
16
|
+
export declare function refreshAccessToken(fetchImpl: typeof fetch, params: {
|
|
17
|
+
clientId: string;
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
refreshToken: string;
|
|
20
|
+
}): Promise<RefreshedToken>;
|
|
21
|
+
export interface DriveQuota {
|
|
22
|
+
/** null = unlimited (Workspace accounts with no cap report this). */
|
|
23
|
+
limitBytes: number | null;
|
|
24
|
+
usageBytes: number;
|
|
25
|
+
freeBytes: number | null;
|
|
26
|
+
}
|
|
27
|
+
export interface DriveAbout {
|
|
28
|
+
email?: string;
|
|
29
|
+
quota: DriveQuota;
|
|
30
|
+
}
|
|
31
|
+
export declare function driveAbout(fetchImpl: typeof fetch, accessToken: string): Promise<DriveAbout>;
|
|
32
|
+
export interface DriveFolder {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
/** App-visible folders (drive.file scope: only ones this app created), most
|
|
37
|
+
* recently modified first. Follows `nextPageToken` and accumulates every
|
|
38
|
+
* page — an author with more than one page of app-created folders would
|
|
39
|
+
* otherwise see the destinations picker silently miss folders past the
|
|
40
|
+
* first 100. (`ensureFolder`'s find-by-name lookup uses its own single
|
|
41
|
+
* server-side query, {@link findFolderByName} below, and doesn't paginate.) */
|
|
42
|
+
export declare function listFolders(fetchImpl: typeof fetch, accessToken: string): Promise<DriveFolder[]>;
|
|
43
|
+
/** Look up one folder by id (for verifying a manifest-recorded `folderId`
|
|
44
|
+
* still exists and isn't trashed). Returns null when it's gone. */
|
|
45
|
+
export declare function getFolderById(fetchImpl: typeof fetch, accessToken: string, folderId: string): Promise<DriveFolder | null>;
|
|
46
|
+
/** Create a folder at My Drive root. */
|
|
47
|
+
export declare function createFolder(fetchImpl: typeof fetch, accessToken: string, name: string): Promise<DriveFolder>;
|
|
48
|
+
/** Find-or-create a folder by name at My Drive root (D5's name-resolution
|
|
49
|
+
* path, used when the manifest has no recorded `folderId` yet). One query,
|
|
50
|
+
* not a client-side scan of every app-created folder — see
|
|
51
|
+
* {@link findFolderByName}. */
|
|
52
|
+
export declare function ensureFolder(fetchImpl: typeof fetch, accessToken: string, name: string): Promise<DriveFolder>;
|
|
53
|
+
export interface DriveFile {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
webViewLink?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Find a file by exact basename inside one folder (app-visible files only,
|
|
59
|
+
* not trashed) — the D6 update-in-place lookup. */
|
|
60
|
+
export declare function findFileInFolder(fetchImpl: typeof fetch, accessToken: string, folderId: string, name: string): Promise<DriveFile | null>;
|
|
61
|
+
/** Must be a multiple of 256 KiB (Drive's resumable-upload chunk requirement). */
|
|
62
|
+
export declare const RESUMABLE_CHUNK_SIZE: number;
|
|
63
|
+
export interface ResumableUploadOptions {
|
|
64
|
+
/** Existing file id → update (PATCH); omitted → create (POST). */
|
|
65
|
+
fileId?: string;
|
|
66
|
+
/** Only used on create — the file's Drive name and parent folder. */
|
|
67
|
+
name: string;
|
|
68
|
+
parentFolderId?: string;
|
|
69
|
+
/** Local path to read the artifact from — read incrementally, never
|
|
70
|
+
* loaded whole into memory. */
|
|
71
|
+
filePath: string;
|
|
72
|
+
totalBytes: number;
|
|
73
|
+
mimeType?: string;
|
|
74
|
+
onProgress?: (uploadedBytes: number, totalBytes: number) => void;
|
|
75
|
+
/** Bytes per chunk. Default {@link RESUMABLE_CHUNK_SIZE}; must stay a
|
|
76
|
+
* multiple of 256 KiB (Drive requirement) for any override (tests only). */
|
|
77
|
+
chunkSize?: number;
|
|
78
|
+
maxRetriesPerChunk?: number;
|
|
79
|
+
/** Injectable sleep for tests (so backoff tests run instantly). */
|
|
80
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resumable upload: start a session, PUT chunks read incrementally from disk,
|
|
84
|
+
* follow `308 Resume Incomplete` + `Range` resumes, retry a failed chunk with
|
|
85
|
+
* backoff, and report progress. Used both for creating a new file (no
|
|
86
|
+
* `fileId`) and updating one in place (D6).
|
|
87
|
+
*/
|
|
88
|
+
export declare function resumableUpload(fetchImpl: typeof fetch, accessToken: string, opts: ResumableUploadOptions): Promise<DriveFile>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google API failure decoding for the `gdrive` publish provider (#221) — the
|
|
3
|
+
* ONE place a non-OK Drive response becomes an author-facing message.
|
|
4
|
+
*
|
|
5
|
+
* Google answers every failed API call with a JSON body of the shape
|
|
6
|
+
* { error: { code, message, status?, errors?: [{ reason, message, domain }] } }
|
|
7
|
+
* and `reason` is the only field that says WHY. Reporting just "HTTP 403" —
|
|
8
|
+
* as the first cut did — left an author with nothing to act on and a
|
|
9
|
+
* maintainer reading the app log with nothing to diagnose. The 0.10.5
|
|
10
|
+
* bring-up hit exactly that: a freshly registered OAuth client whose Cloud
|
|
11
|
+
* project had the Drive API disabled, every call answering
|
|
12
|
+
* `403 accessNotConfigured`, and the app saying only "HTTP 403". This module
|
|
13
|
+
* keeps the reason and Google's own message (which, for that case, carries
|
|
14
|
+
* the enable-it link with the project id) and maps the well-known reasons to
|
|
15
|
+
* a sentence that says what to do.
|
|
16
|
+
*
|
|
17
|
+
* Invariant: every message produced here names "Google" as prose. The
|
|
18
|
+
* desktop's publish error allowlist (electron/server-bridge/friendly-errors.ts,
|
|
19
|
+
* `\bgoogle\b`) keys on that to pass a lib message through to the author
|
|
20
|
+
* verbatim instead of masking it behind "See the app log for details" — which
|
|
21
|
+
* is what happened to "Couldn't create the Drive folder …" before this module
|
|
22
|
+
* existed.
|
|
23
|
+
*
|
|
24
|
+
* Token-free by construction: Google's error messages describe the API,
|
|
25
|
+
* project, scope, or quota — never the bearer token — and a Drive error body
|
|
26
|
+
* does not echo the request URL. `redactQueryCredentials` is belt-and-braces
|
|
27
|
+
* for the one shape that could ever carry one.
|
|
28
|
+
*/
|
|
29
|
+
import { FriendlyHttpError } from "../fetch-timeout.ts";
|
|
30
|
+
export interface GoogleApiErrorInfo {
|
|
31
|
+
status: number;
|
|
32
|
+
/** Google's machine reason (`accessNotConfigured`, `insufficientPermissions`, …), when present. */
|
|
33
|
+
reason?: string;
|
|
34
|
+
/** Google's human-readable message, when present. */
|
|
35
|
+
message?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Decode Google's standard error envelope. Tolerates any body shape — a
|
|
38
|
+
* non-JSON or unexpected body just yields the bare status. */
|
|
39
|
+
export declare function parseGoogleApiError(status: number, body: unknown): GoogleApiErrorInfo;
|
|
40
|
+
/** Read a non-OK response's body once and decode it. */
|
|
41
|
+
export declare function readGoogleApiError(res: Response): Promise<GoogleApiErrorInfo>;
|
|
42
|
+
/**
|
|
43
|
+
* Build the author-facing error for a failed Google API call: `what` is the
|
|
44
|
+
* caller's "Couldn't …" phrase; the status and Google's reason follow in
|
|
45
|
+
* parentheses, then the what-to-do hint for a known reason, then Google's own
|
|
46
|
+
* message. Always a {@link FriendlyHttpError} (passes through
|
|
47
|
+
* `withFetchTimeout` unwrapped, like every other Drive failure).
|
|
48
|
+
*/
|
|
49
|
+
export declare function googleApiFailure(what: string, info: GoogleApiErrorInfo): FriendlyHttpError;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type PublishProvider } from "../types.ts";
|
|
2
|
+
/** What actually gets uploaded: a local file path + the Drive-visible name.
|
|
3
|
+
* For the PDF format this IS the artifact; for HTML (D8) it's the zipped
|
|
4
|
+
* export, in a temp file `cleanup` removes once the upload finishes (or
|
|
5
|
+
* fails) — the upload path itself doesn't care which one it got. */
|
|
6
|
+
interface UploadSource {
|
|
7
|
+
filePath: string;
|
|
8
|
+
fileName: string;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
cleanup: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Package the HTML export directory into a single ZIP (D8: Drive is file
|
|
14
|
+
* delivery, not web hosting — no N-file folder mirroring). Built via
|
|
15
|
+
* `zipEntriesNonBlocking` (fflate is already a dependency, see
|
|
16
|
+
* theme-import.ts for the sibling unzip-side usage) so building the archive
|
|
17
|
+
* doesn't block the event loop, then written to a temp file so the existing
|
|
18
|
+
* file-based `resumableUpload` (google-drive.ts) can read it incrementally
|
|
19
|
+
* like any other artifact. Note this is true only of the UPLOAD step — the
|
|
20
|
+
* ZIP-BUILD step above necessarily holds the whole compressed archive in
|
|
21
|
+
* memory before it's written out below.
|
|
22
|
+
*/
|
|
23
|
+
export declare function zipHtmlExport(exportDir: string, title: string, onWarn?: (message: string) => void, writeArchiveImpl?: (filePath: string, data: Uint8Array) => Promise<void>): Promise<UploadSource>;
|
|
24
|
+
export declare const gdriveProvider: PublishProvider;
|
|
25
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PreflightIssue, PublishDeps, PublishOutcome, PublishRequest } from "./types.ts";
|
|
1
|
+
import type { PreflightIssue, PublishArtifactFormat, PublishDeps, PublishOutcome, PublishProviderInfo, PublishRequest } from "./types.ts";
|
|
2
2
|
export interface RunPublishOptions {
|
|
3
3
|
projectDir: string;
|
|
4
4
|
providerId: string;
|
|
@@ -20,6 +20,19 @@ export interface RunPublishResult {
|
|
|
20
20
|
/** Friendly failure summary when `ok` is false. */
|
|
21
21
|
error?: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute the EFFECTIVE artifact format for a publish request (#221 phase 3,
|
|
25
|
+
* D8). Every provider except gdrive declares only a single fixed
|
|
26
|
+
* `info.format` and has no `info.formats` array — for those, this function
|
|
27
|
+
* always returns `info.format`, so their behavior is byte-for-byte
|
|
28
|
+
* unaffected by this change. For a provider that DOES declare `formats`
|
|
29
|
+
* (currently only gdrive: `["pdf", "html"]`), the author's manifest
|
|
30
|
+
* `publish.<id>.format` selects the effective one — but only when it's a
|
|
31
|
+
* value the provider actually declares; an unset, invalid, or typo'd value
|
|
32
|
+
* is IGNORED and falls back to `info.format` rather than blocking publish
|
|
33
|
+
* with a preflight error over one bad manifest field.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolvePublishFormat(info: PublishProviderInfo, providerConfig: Record<string, unknown>): PublishArtifactFormat;
|
|
23
36
|
/**
|
|
24
37
|
* Resolve the {@link PublishRequest} for a provider from the project's
|
|
25
38
|
* manifest — shared by `runPublish` and by hosts that call individual
|
|
@@ -33,7 +33,7 @@ export interface PublishConfigField {
|
|
|
33
33
|
}
|
|
34
34
|
/** Stable provider identifiers: the `--provider` values AND the manifest
|
|
35
35
|
* `publish.<id>` keys — one spelling everywhere. */
|
|
36
|
-
export type PublishProviderId = "itch" | "drivethrurpg" | "kdp" | "azure-swa" | "shopify";
|
|
36
|
+
export type PublishProviderId = "itch" | "drivethrurpg" | "kdp" | "azure-swa" | "shopify" | "gdrive";
|
|
37
37
|
/**
|
|
38
38
|
* How the provider integrates:
|
|
39
39
|
* - "api" — real programmatic upload (itch.io, Azure SWA, Shopify)
|
|
@@ -49,8 +49,20 @@ export interface PublishProviderInfo {
|
|
|
49
49
|
/** Human name ("itch.io"). */
|
|
50
50
|
label: string;
|
|
51
51
|
kind: PublishProviderKind;
|
|
52
|
-
/** The artifact format this provider publishes
|
|
52
|
+
/** The artifact format this provider publishes BY DEFAULT — and, for every
|
|
53
|
+
* provider except those declaring {@link formats}, the ONLY one it ever
|
|
54
|
+
* publishes. */
|
|
53
55
|
format: PublishArtifactFormat;
|
|
56
|
+
/**
|
|
57
|
+
* Formats this provider can ALSO publish, when it supports more than one
|
|
58
|
+
* (#221 phase 3, D8 — currently only gdrive: `["pdf", "html"]`). Absent for
|
|
59
|
+
* every other provider, which keeps them on the single fixed `format`
|
|
60
|
+
* above with no behavior change. When present, the author's manifest
|
|
61
|
+
* `publish.<id>.format` (validated against this list) picks the EFFECTIVE
|
|
62
|
+
* format for one publish request — see `run-publish.ts`'s
|
|
63
|
+
* `resolvePublishFormat`.
|
|
64
|
+
*/
|
|
65
|
+
formats?: PublishArtifactFormat[];
|
|
54
66
|
/** One-line author-facing description of what publishing here does. */
|
|
55
67
|
description: string;
|
|
56
68
|
/** The provider's author-editable manifest settings. */
|
|
@@ -69,6 +81,30 @@ export interface PublishProviderInfo {
|
|
|
69
81
|
tokenUrl?: string;
|
|
70
82
|
/** Author-facing hint ("Paste an API key from …"). */
|
|
71
83
|
hint?: string;
|
|
84
|
+
/**
|
|
85
|
+
* How the credential is acquired. "token" (default, and every provider's
|
|
86
|
+
* behavior before this field existed): the author pastes an API key,
|
|
87
|
+
* verified by {@link connectPublishProvider}(pasted-token flow). "oauth":
|
|
88
|
+
* there is no key to paste — the credential comes from an interactive
|
|
89
|
+
* browser consent flow (see `google-auth.ts`'s `GoogleAuthProvider` for
|
|
90
|
+
* the gdrive precedent). UIs branch on this to swap the paste-a-key form
|
|
91
|
+
* for a "Connect …" button; {@link connectPublishProvider} REJECTS oauth
|
|
92
|
+
* providers outright so the old paste-a-token path can never store an
|
|
93
|
+
* unverifiable credential for one (#221).
|
|
94
|
+
*/
|
|
95
|
+
connect?: "token" | "oauth";
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Present when the provider has a notion of "existing places to publish
|
|
99
|
+
* into" that a UI can let the author pick or create (gdrive: Drive
|
|
100
|
+
* folders) — precedent: the existing optional `listProducts`. `label` is
|
|
101
|
+
* the author-facing noun ("Folder"); `canCreate` says whether
|
|
102
|
+
* {@link PublishProvider.createDestination} is implemented. Absent for
|
|
103
|
+
* providers with no such concept.
|
|
104
|
+
*/
|
|
105
|
+
destinations?: {
|
|
106
|
+
label: string;
|
|
107
|
+
canCreate: boolean;
|
|
72
108
|
};
|
|
73
109
|
}
|
|
74
110
|
/** The project being published (resolved from the manifest by the orchestrator). */
|
|
@@ -218,6 +254,14 @@ export interface PublishProvider {
|
|
|
218
254
|
listProducts?(req: PublishRequest): Promise<PublishProduct[]>;
|
|
219
255
|
/** Update an existing listing's metadata. API providers only. */
|
|
220
256
|
updateListing?(req: PublishRequest, productId: string, metadata: PublishListingMetadata): Promise<PublishProduct>;
|
|
257
|
+
/**
|
|
258
|
+
* Existing places this provider can publish into (gdrive: app-visible
|
|
259
|
+
* Drive folders). UIs render a picker when this is implemented — see
|
|
260
|
+
* {@link PublishProviderInfo.destinations}.
|
|
261
|
+
*/
|
|
262
|
+
listDestinations?(req: PublishRequest): Promise<PublishProduct[]>;
|
|
263
|
+
/** Create a new destination (gdrive: a Drive folder at My Drive root). */
|
|
264
|
+
createDestination?(req: PublishRequest, name: string): Promise<PublishProduct>;
|
|
221
265
|
}
|
|
222
266
|
/**
|
|
223
267
|
* Resolve the credential for a provider: the CI env var wins, then the token
|
|
@@ -3,7 +3,7 @@ export interface HostCredential {
|
|
|
3
3
|
/** Remote host the credential authenticates against, e.g. "github.com". */
|
|
4
4
|
host: string;
|
|
5
5
|
/** How the credential was acquired (drives re-auth UX, not transport). */
|
|
6
|
-
kind: "github-oauth" | "token";
|
|
6
|
+
kind: "github-oauth" | "token" | "google-oauth";
|
|
7
7
|
/** The secret token value. NEVER log this. */
|
|
8
8
|
token: string;
|
|
9
9
|
/** Login/username associated with the token, when known. */
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MANIFEST_FILENAMES,
|
|
3
3
|
runLint
|
|
4
|
-
} from "./cli-
|
|
4
|
+
} from "./cli-5w9y6r2f.js";
|
|
5
5
|
import {
|
|
6
6
|
log
|
|
7
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-nb902265.js";
|
|
8
8
|
import {
|
|
9
9
|
UsageError,
|
|
10
10
|
rejectExtraPositionals,
|
|
11
11
|
rejectUnknownFlags
|
|
12
|
-
} from "./cli-
|
|
13
|
-
import"./cli-
|
|
12
|
+
} from "./cli-xewqry0j.js";
|
|
13
|
+
import"./cli-0t4zfevc.js";
|
|
14
14
|
import"./cli-c41yr7he.js";
|
|
15
15
|
import {
|
|
16
16
|
EXIT_CODES
|