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.
Files changed (38) hide show
  1. package/README.md +7 -3
  2. package/dist/api/index.d.ts +7 -3
  3. package/dist/api/index.js +24 -2
  4. package/dist/{audit-6gsbjcyp.js → audit-9pf96y1w.js} +4 -4
  5. package/dist/{build-8gx7gkgw.js → build-jw9kynts.js} +4 -4
  6. package/dist/{cli-7hay1r0h.js → cli-0t4zfevc.js} +1 -1
  7. package/dist/{cli-zddg7r27.js → cli-5w9y6r2f.js} +928 -74
  8. package/dist/{cli-h617210r.js → cli-nb902265.js} +1 -1
  9. package/dist/{cli-d613m9n5.js → cli-xewqry0j.js} +1 -1
  10. package/dist/cli.js +14 -14
  11. package/dist/{doctor-mrqpw7g3.js → doctor-6vmxh7mz.js} +2 -2
  12. package/dist/{engine-dzrzyyew.js → engine-fwe8djyn.js} +1 -1
  13. package/dist/{engine-pta3yeyt.js → engine-mtyjc4v6.js} +2 -2
  14. package/dist/{index-1z8a090c.js → index-3xv7vwv2.js} +972 -120
  15. package/dist/{index-cm4dywtk.js → index-ja0p4w5f.js} +1 -1
  16. package/dist/{index-p4yq14qw.js → index-k5hcp8wj.js} +1 -1
  17. package/dist/index.js +25 -3
  18. package/dist/lib/markdown/gutterpress-css.d.ts +10 -2
  19. package/dist/lib/open-path.d.ts +27 -0
  20. package/dist/lib/publish/connect-google.d.ts +34 -0
  21. package/dist/lib/publish/connect.d.ts +23 -0
  22. package/dist/lib/publish/google-auth.d.ts +104 -0
  23. package/dist/lib/publish/google-drive.d.ts +88 -0
  24. package/dist/lib/publish/google-errors.d.ts +49 -0
  25. package/dist/lib/publish/providers/gdrive.d.ts +25 -0
  26. package/dist/lib/publish/run-publish.d.ts +14 -1
  27. package/dist/lib/publish/types.d.ts +46 -2
  28. package/dist/lib/remote-auth/token-store.d.ts +1 -1
  29. package/dist/{lint-gmymvnm3.js → lint-kzvbrn3d.js} +4 -4
  30. package/dist/{manifest.schema-084rxtwp.json → manifest.schema-hn6ac0ae.json} +25 -0
  31. package/dist/{new-hw7s7jne.js → new-cyna8tyn.js} +4 -4
  32. package/dist/{plugin-0qtfgjr2.js → plugin-mpc5v8sr.js} +4 -4
  33. package/dist/{preflight-j99dpj5e.js → preflight-fc178as2.js} +4 -4
  34. package/dist/{preview-sc3rcrkh.js → preview-qnbbtyqb.js} +4 -4
  35. package/dist/{publish-xwrwvrh0.js → publish-gmgpyh4n.js} +58 -9
  36. package/dist/schema/manifest.types.d.ts +20 -0
  37. package/dist/{validate-evzfg9m6.js → validate-0t3vjpmk.js} +4 -4
  38. package/package.json +1 -1
@@ -4,7 +4,7 @@ import {
4
4
  launchChromium,
5
5
  marginBoxAlign,
6
6
  marginBoxRectPt
7
- } from "./index-p4yq14qw.js";
7
+ } from "./index-k5hcp8wj.js";
8
8
 
9
9
  // src/engine/compiler/build.ts
10
10
  import { readFile } from "node:fs/promises";
@@ -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-084rxtwp.json";
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-1z8a090c.js";
127
+ } from "./index-3xv7vwv2.js";
117
128
  import {
118
129
  BUILD_DIAGNOSTIC_CODES
119
- } from "./index-cm4dywtk.js";
130
+ } from "./index-ja0p4w5f.js";
120
131
  import {
121
132
  BuildError,
122
133
  DEFAULT_PRINT_OPTS,
123
134
  readyProbeExpr
124
- } from "./index-p4yq14qw.js";
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 the nearest @page/@spread container;
42
- * centered on both axes unless combined with the edge
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:
@@ -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-zddg7r27.js";
4
+ } from "./cli-5w9y6r2f.js";
5
5
  import {
6
6
  log
7
- } from "./cli-h617210r.js";
7
+ } from "./cli-nb902265.js";
8
8
  import {
9
9
  UsageError,
10
10
  rejectExtraPositionals,
11
11
  rejectUnknownFlags
12
- } from "./cli-d613m9n5.js";
13
- import"./cli-7hay1r0h.js";
12
+ } from "./cli-xewqry0j.js";
13
+ import"./cli-0t4zfevc.js";
14
14
  import"./cli-c41yr7he.js";
15
15
  import {
16
16
  EXIT_CODES