pendingyou 0.29.0 → 0.30.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.30.0 (2026-10-07)
4
+
5
+ - This computer's own sign-ins (the connections `init` made for Claude Code, Codex, OpenCode and Pi) now last while
6
+ you use them. Each refresh carries a proof by this computer's key (a DPoP proof, RFC 9449, signed with the key in
7
+ `~/.config/pendingyou/machine.json`), and Pending You renews a sign-in for 30 days from that refresh when it's proven
8
+ by the key of the computer it's on. Before, every sign-in ended 30 days after it began, however much it was used. A
9
+ computer left unused for 30 days still signs out; Pending You says so 3 days before. Its tokens are unchanged, and a
10
+ refresh never fails for want of a proof: without a key, or when the proof isn't taken, it refreshes as before.
11
+
3
12
  ## 0.29.0 (2026-10-07)
4
13
 
5
14
  - The command line links this computer's sign-ins to it by its key, so Pending You names every assistant here by this
package/README.md CHANGED
@@ -339,8 +339,8 @@ hold to approve. Pending You says on the card and to the agent "Answered in Herd
339
339
  Herdr (moving between panes, tabs and workspaces, or pressing keys in the popup) your phone stays quiet for those cards,
340
340
  as it does while Pending You is open on your desk.
341
341
 
342
- `npx pendingyou app status` says what each app signed in here may do and until when (a sign-in lasts 30 days; it warns
343
- in the last 3), and `npx pendingyou app logout herdr` ends Herdr's sign-in. Settings › Apps and devices in Pending You
342
+ `npx pendingyou app status` says what each app signed in here may do and until when (a sign-in lasts 30 days from its
343
+ last use here; it warns in the last 3), and `npx pendingyou app logout herdr` ends Herdr's sign-in. Settings › Apps and devices in Pending You
344
344
  lists it too, with Remove. It needs `init` to have run here first: this computer's key vouches for the sign-in.
345
345
 
346
346
  ## How Claude Code hears your answer
@@ -440,8 +440,8 @@ The machine is called by its Tailscale name when Tailscale is installed and runn
440
440
  else by its hostname, or by `--name build-01` on `init` or `login`; a Mac by its own name (Sharing in System Settings).
441
441
  A cloud's own hostname (AWS's `ip-10-42-1-252`) is kept when there's nothing better, and `login` says how to change it:
442
442
  `npx pendingyou login --name <name>` asks for one more approval and renames the connection, which stays the same one.
443
- If an agent's sign-in ends (30 days, or you remove it on your Assistants page), `npx pendingyou login` (`--app codex`
444
- for Codex's) approves a new code and keeps the same connection.
443
+ If an agent's sign-in ends (30 days unused, or you remove it on your Assistants page), `npx pendingyou login`
444
+ (`--app codex` for Codex's; `--force` while it still works) approves a new code and keeps the same connection.
445
445
 
446
446
  Only want this command's own sign-in (for `watch`, below)? `npx pendingyou login --device` signs it in with a code,
447
447
  hearing answers only, and adds no connection. Your phone asks "Let build-01 hear your answers for the assistants
@@ -527,8 +527,9 @@ The hooks use the same sign-in, and hand a session only the cards its own app po
527
527
  signs in by itself (`--oauth`, or one you kept) gets a sign-in that only hears its answers instead: it can't ask,
528
528
  answer or change anything, and doesn't show up as an assistant. They're stored in `~/.config/pendingyou/credentials.json`
529
529
  (readable only by you, in a folder with a `.gitignore` of `*` so a dotfiles repository never takes it), refresh
530
- themselves, and end after 30 days or on `logout`. The headers helper hands an agent's token only to that agent's
531
- Pending You (Claude Code says which server it's for; Codex's is the one `init` wrote). `clients.json` remembers which
530
+ themselves, last 30 days from their last use (0.30.0: each refresh proves this computer's key, and Pending You renews
531
+ the sign-in from then), and end once unused that long, or on `logout`. The headers helper hands an agent's token only
532
+ to that agent's Pending You (Claude Code says which server it's for; Codex's is the one `init` wrote). `clients.json` remembers which
532
533
  sign-in each agent's connection used, so signing in again keeps it. `machine.json` (0.18.0) holds this computer's key
533
534
  pair, private like the rest; only its proofs leave the computer, each for one sign-in, once. `apps/<app>.json` (0.21.0)
534
535
  holds an app's own sign-in and the private key its tokens are bound to (DPoP), private like the rest; the Pending You
package/dist/api.js CHANGED
@@ -33,8 +33,11 @@ async function refreshStored(io, origin, stored, options, app) {
33
33
  return current;
34
34
  if (!options.force && current.expiresAt - options.earlyMs > io.now())
35
35
  return current;
36
+ const prove = current.kind === 'connection'
37
+ ? await (await import("./machine.js")).machineProver(io)
38
+ : undefined;
36
39
  try {
37
- const tokens = await refresh(io, origin, current.clientId, current.refreshToken);
40
+ const tokens = await refresh(io, origin, current.clientId, current.refreshToken, prove);
38
41
  return { ...current, ...tokens };
39
42
  }
40
43
  catch (error) {
package/dist/machine.js CHANGED
@@ -7,6 +7,7 @@ import { PlainError } from "./errors.js";
7
7
  import { configDir, readJson, withLock, writeWhole } from "./files.js";
8
8
  import { discover } from "./oauth.js";
9
9
  import { machineOf } from "./remote.js";
10
+ import { proof as dpopProof } from "./sdk/dpop.js";
10
11
  export const ATTESTATION_TYPE = 'pendingyou-machine+jwt';
11
12
  const ATTESTATION_SECONDS = 300;
12
13
  export const MACHINE_PATH = '/mcp/cli/machine';
@@ -100,6 +101,13 @@ export async function attest(machine, claims, now) {
100
101
  const signature = await webcrypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, machine.signer, Buffer.from(signing));
101
102
  return `${signing}.${base64url(new Uint8Array(signature))}`;
102
103
  }
104
+ export async function machineProver(io) {
105
+ const machine = await readMachine(io).catch(() => null);
106
+ if (!machine)
107
+ return undefined;
108
+ const key = { jwk: machine.jwk, jkt: machine.thumbprint, privateKey: machine.signer };
109
+ return ({ method, url, nonce, now }) => dpopProof(key, { method, url, now, ...(nonce ? { nonce } : {}) });
110
+ }
103
111
  export async function keyForSignIn(io) {
104
112
  try {
105
113
  const { machine, made } = await ensureMachine(io);
package/dist/oauth.js CHANGED
@@ -128,14 +128,28 @@ async function listen(state, signal) {
128
128
  },
129
129
  };
130
130
  }
131
- async function tokenRequest(io, metadata, params, timeoutMs = 20_000) {
132
- const response = await io.fetch(metadata.token_endpoint, {
131
+ async function tokenRequest(io, metadata, params, timeoutMs = 20_000, prove) {
132
+ const signal = AbortSignal.timeout(timeoutMs);
133
+ const send = async (dpop) => io.fetch(metadata.token_endpoint, {
133
134
  method: 'POST',
134
- headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
135
+ headers: {
136
+ 'content-type': 'application/x-www-form-urlencoded',
137
+ accept: 'application/json',
138
+ ...(dpop ? { dpop } : {}),
139
+ },
135
140
  body: new URLSearchParams(params).toString(),
136
- signal: AbortSignal.timeout(timeoutMs),
141
+ signal,
137
142
  });
138
- const body = (await response.json().catch(() => ({})));
143
+ const proof = (nonce) => (prove ? prove(nonce).catch(() => null) : null);
144
+ let response = await send(await proof());
145
+ let body = (await response.json().catch(() => ({})));
146
+ for (const again of ['nonce', 'plain']) {
147
+ if (body.error !== 'use_dpop_nonce')
148
+ break;
149
+ const nonce = response.headers.get('dpop-nonce');
150
+ response = await send(again === 'nonce' && nonce ? await proof(nonce) : null);
151
+ body = (await response.json().catch(() => ({})));
152
+ }
139
153
  if (!response.ok ||
140
154
  typeof body.access_token !== 'string' ||
141
155
  typeof body.refresh_token !== 'string') {
@@ -192,14 +206,21 @@ export async function signIn(io, options) {
192
206
  loopback.close();
193
207
  }
194
208
  }
195
- export async function refresh(io, origin, clientId, refreshToken) {
209
+ export async function refresh(io, origin, clientId, refreshToken, prove) {
196
210
  const metadata = await discover(io, origin, 8_000);
197
211
  return tokenRequest(io, metadata, {
198
212
  grant_type: 'refresh_token',
199
213
  refresh_token: refreshToken,
200
214
  client_id: clientId,
201
215
  resource: resourceOf(origin),
202
- }, 12_000);
216
+ }, 12_000, prove
217
+ ? (nonce) => prove({
218
+ method: 'POST',
219
+ url: metadata.token_endpoint,
220
+ now: serverNow(io, metadata),
221
+ ...(nonce ? { nonce } : {}),
222
+ })
223
+ : undefined);
203
224
  }
204
225
  export async function revoke(io, origin, clientId, refreshToken) {
205
226
  try {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.29.0';
1
+ export const VERSION = '0.30.0';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pendingyou-wake",
3
3
  "displayName": "Pending You",
4
- "version": "2.44.0",
4
+ "version": "2.44.1",
5
5
  "description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
6
6
  "author": {
7
7
  "name": "RecordPlane",
package/mod/README.md CHANGED
@@ -4,4 +4,4 @@ The pendingyou command line’s copy of the Pending You plugin’s wake mod, for
4
4
 
5
5
  In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
6
6
 
7
- Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.44.0; send changes there.
7
+ Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.44.1; send changes there.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://www.pendingyou.com",