pullfrog 0.1.64 → 0.1.66

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.
@@ -57,11 +57,7 @@ export declare function readLearningsFile(path: string): Promise<string | null>;
57
57
  * Best-effort: any failure is logged and does not affect the run's success
58
58
  * status. Skips the PATCH when the file is byte-trim-identical to its seed —
59
59
  * the agent didn't touch it, so writing the same content back would just
60
- * burn a `LearningsRevision` row and an API round-trip.
61
- *
62
- * `ctx.toolState.model` is forwarded so `LearningsRevision.model` keeps
63
- * populating; it powers the per-revision attribution badge in the UI
64
- * history view.
60
+ * burn an API round-trip.
65
61
  *
66
62
  * `learningsPersistAttempted` guards against double-execution between the
67
63
  * normal end-of-run path and the SIGINT/SIGTERM handler.
@@ -5,8 +5,8 @@
5
5
  * `action/internal/index.ts` without dragging the entire MCP type graph
6
6
  * along — `learnings.ts` imports `ToolContext` for its runtime helpers,
7
7
  * and pulling that into the SDK-facing `internal` barrel expands the
8
- * type graph reachable from root `tsc` and `cf-worker-indexing` to every
9
- * tool module under `action/mcp/`. keeping these helpers MCP-free is the
8
+ * type graph reachable from root `tsc` to every tool module under
9
+ * `action/mcp/`. keeping these helpers MCP-free is the
10
10
  * cheap structural fix.
11
11
  *
12
12
  * see `action/utils/learnings.ts` for the full learnings-file lifecycle.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Provider-agnostic OAuth primitives shared by the Codex and Grok chains.
3
+ * Pure stdlib (Buffer) so both the action runtime and the Next.js server can
4
+ * import it via pullfrog/internal.
5
+ *
6
+ * Both chains mint JWT access tokens whose `exp` we read as a freshness hint,
7
+ * and both rotate their refresh token on every use — so both need the same
8
+ * "the provider rejected this permanently" signal.
9
+ */
10
+ /**
11
+ * Thrown when an OAuth provider rejects a refresh token (4xx).
12
+ *
13
+ * `chainIsDead` is decided by the CALLER, because the two providers we talk to
14
+ * disagree about how to say it and only the caller knows its own dialect:
15
+ * xAI answers RFC 6749 (`{"error":"invalid_grant"}`) while OpenAI nests an
16
+ * object and discriminates on `error.code` (`token_expired`). A rejection we
17
+ * cannot classify — a CDN error page in front of the token endpoint, say — is
18
+ * NOT dead: latching there would retire every customer's working credential
19
+ * over a transient edge event.
20
+ */
21
+ export declare class OAuthInvalidGrantError extends Error {
22
+ readonly status: number;
23
+ /** whether the provider said this refresh chain is permanently unusable, so
24
+ * the rotation core may latch it out of use until the user re-mints. */
25
+ readonly chainIsDead: boolean;
26
+ constructor(provider: string, status: number, body: string, chainIsDead: boolean);
27
+ }
28
+ /** parse a token-endpoint error body, or null when it is not a JSON object.
29
+ * Each provider's refresh reads its own discriminator out of the result. */
30
+ export declare function parseOAuthErrorBody(body: string): Record<string, unknown> | null;
31
+ /** decode a JWT payload's `exp` claim and return it in ms since epoch.
32
+ * returns null if the token isn't a parseable JWT or has no `exp` claim —
33
+ * caller falls back to "treat as expired".
34
+ *
35
+ * We don't verify the signature (we'd need the issuer's JWKS); we're only
36
+ * using the claim as a freshness hint. The real auth check happens at the
37
+ * provider when the token is used, so trusting a forged JWT here would just
38
+ * delay the inevitable 401. No security boundary at this decode step. */
39
+ export declare function decodeJwtExpMs(token: string): number | null;
@@ -19,6 +19,7 @@ export declare const JsonPayload: import("arktype/internal/variants/object.ts").
19
19
  write: string[];
20
20
  unavailable?: string[];
21
21
  } | undefined;
22
+ xrepoGrant?: string | undefined;
22
23
  timeout?: string | undefined;
23
24
  progressComment?: {
24
25
  id: string;
@@ -66,6 +67,7 @@ export declare function resolvePayload(resolvedPromptInput: ResolvedPromptInput,
66
67
  write: string[];
67
68
  unavailable?: string[];
68
69
  } | undefined;
70
+ xrepoGrant: string | undefined;
69
71
  timeout: string | undefined;
70
72
  cwd: string | undefined;
71
73
  progressComment: {
@@ -29,6 +29,12 @@ type ResolveTokensParams = {
29
29
  push: PushPermission;
30
30
  authorPermission: AuthorPermission | undefined;
31
31
  xrepo?: XrepoConfig | undefined;
32
+ /**
33
+ * handle to the cross-repo grant the server persisted for this run. required
34
+ * alongside `xrepo`: the mint endpoint refuses a `repos` list without one,
35
+ * because the runner's list is attacker-controlled on a manual dispatch.
36
+ */
37
+ xrepoGrant?: string | undefined;
32
38
  /**
33
39
  * OIDC credentials stashed by main.ts before the restricted-mode env wipe —
34
40
  * the mid-run MCP token refresh mints from this snapshot (#891). null when
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pure-stdlib (fetch) xAI/Grok OAuth: device-code login + refresh.
3
+ *
4
+ * Lives here (not in a CLI module) so the Next.js server side can import it
5
+ * via pullfrog/internal without dragging in node:child_process. Used by:
6
+ * - action/commands/auth.ts (`pullfrog auth grok` device-code login)
7
+ * - action/utils/codexHome.ts (materialize into opencode's auth.json)
8
+ * - utils/xaiSecretRotation.ts (server-side rotation at run-context)
9
+ *
10
+ * We talk to xAI's OAuth endpoints directly rather than shelling out to the
11
+ * Grok CLI the way `auth codex` shells out to `codex login`. The Grok CLI is
12
+ * a curl|bash install that nothing else in the flow needs, and the device
13
+ * grant is ~60 lines of fetch — requiring a second CLI to obtain a
14
+ * credential opencode consumes natively would be gratuitous.
15
+ *
16
+ * See wiki/grok-auth.md.
17
+ */
18
+ export interface XaiAuthBody {
19
+ auth_mode: "grok";
20
+ tokens: {
21
+ access_token: string;
22
+ refresh_token: string;
23
+ };
24
+ last_refresh?: string;
25
+ /**
26
+ * ISO timestamp of an `invalid_grant` rejection. xAI rotates the refresh
27
+ * token on every use, so a rejection is PERMANENT until the user re-runs
28
+ * `pullfrog auth grok`. Without the latch the server re-issues the same
29
+ * doomed refresh on every run, each holding a Postgres row lock across an
30
+ * external call — the failure mode measured for Codex in
31
+ * [#1101](https://github.com/pullfrog/app/issues/1101). Cleared implicitly:
32
+ * `pullfrog auth grok` and `PUT /api/runtime/secret` write a fresh blob
33
+ * without it, which re-arms rotation.
34
+ */
35
+ refresh_rejected_at?: string;
36
+ }
37
+ /** Public Grok-CLI OAuth client. Identical to the `oidc_client_id` the Grok
38
+ * CLI writes into its own `~/.grok/auth.json` and to opencode's XaiAuthPlugin
39
+ * CLIENT_ID — one chain, so a token minted here refreshes anywhere. */
40
+ export declare const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
41
+ export declare const XAI_OAUTH_TOKEN_URL = "https://auth.x.ai/oauth2/token";
42
+ export declare const XAI_DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code";
43
+ export declare const XAI_DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
44
+ /** `api:access` is the scope that lets the subscription token authorize
45
+ * api.x.ai — without it the credential logs in but cannot infer. */
46
+ export declare const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
47
+ /** force one refresh round-trip against xAI. returns the rotated blob; does
48
+ * NOT persist — the caller writes it back wherever the token lives.
49
+ *
50
+ * The 10s timeout matters server-side: `maybeRotateXaiSecret` holds a DB row
51
+ * lock across this call, so the cap keeps us inside the enclosing transaction
52
+ * budget and guarantees queued callers get a turn. Real latency is sub-second. */
53
+ export declare function refreshXaiAuthBody(body: XaiAuthBody): Promise<XaiAuthBody>;
54
+ /** parse + validate a stored Grok blob. returns null on any shape mismatch —
55
+ * caller treats null as "no grok auth". */
56
+ export declare function parseXaiAuthBody(raw: string): XaiAuthBody | null;
57
+ /** serialize to the canonical stored form. */
58
+ export declare function stringifyXaiAuthBody(body: XaiAuthBody): string;
59
+ export interface XaiDeviceCode {
60
+ deviceCode: string;
61
+ userCode: string;
62
+ /** pre-filled URL when xAI supplies one, else the bare verification URL. */
63
+ verificationUrl: string;
64
+ intervalMs: number;
65
+ expiresAtMs: number;
66
+ }
67
+ /** RFC 8628 step 1: ask xAI for a device code. The user approves in a browser
68
+ * on any device — no loopback callback server, so this works unchanged from a
69
+ * container, an SSH session, or a locked-down workstation. */
70
+ export declare function startXaiDeviceAuth(): Promise<XaiDeviceCode>;
71
+ /** RFC 8628 step 2: long-poll the token endpoint until the user approves.
72
+ * Honors the spec's `authorization_pending` / `slow_down` back-off. Resolves
73
+ * with the minted chain, or throws on denial / expiry. */
74
+ export declare function pollXaiDeviceAuth(device: XaiDeviceCode): Promise<XaiAuthBody>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.64",
3
+ "version": "0.1.66",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",