@poppykit/client 0.4.0 → 0.5.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/dist/provider.js CHANGED
@@ -45,7 +45,6 @@ function https(value) { try {
45
45
  catch {
46
46
  return fail('binding_conflict');
47
47
  } }
48
- function onDomain(value, domain) { const host = https(value).hostname; return host === domain || host.endsWith(`.${domain}`); }
49
48
  function publicKey(value) {
50
49
  if (value.kty !== 'EC' || value.crv !== 'P-256' || typeof value.x !== 'string' || typeof value.y !== 'string' || 'd' in value || 'k' in value)
51
50
  fail('unavailable');
@@ -67,6 +66,8 @@ export function createProvider(options) {
67
66
  const row = tx.get('operation', `${enrollment.enrollmentId}:${operationId}`);
68
67
  if (!row)
69
68
  fail('binding_conflict');
69
+ if (row.binding.purpose === 'direct' && (typeof row.binding.sessionKey !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(row.binding.sessionKey)))
70
+ fail('binding_conflict');
70
71
  if (row.expiresAt <= now())
71
72
  fail('expired');
72
73
  if (row.status === 'canceled')
@@ -138,37 +139,29 @@ export function createProvider(options) {
138
139
  });
139
140
  },
140
141
  async begin(credential, input) {
141
- exact(input, ['version', 'installationId', 'operationId', 'domain', 'issuer', 'purpose', 'sessionId', 'returnTo', 'state']);
142
- if (!['client-authentication', 'session', 'browser', 'direct'].includes(input.purpose))
142
+ exact(input, ['version', 'installationId', 'operationId', 'domain', 'issuer', 'purpose', 'sessionId', 'sessionKey', 'state']);
143
+ if (!['client-authentication', 'session', 'direct'].includes(input.purpose))
143
144
  fail('binding_conflict');
144
145
  text(input.operationId);
145
146
  if (input.sessionId !== undefined)
146
147
  text(input.sessionId);
147
148
  await authorizeCompany(credential, input);
148
- if (input.purpose === 'client-authentication' && (input.sessionId !== undefined || input.returnTo !== undefined))
149
- fail('binding_conflict');
150
- if (input.purpose !== 'browser' && input.returnTo !== undefined)
149
+ if (input.purpose === 'client-authentication' && input.sessionId !== undefined)
151
150
  fail('binding_conflict');
152
151
  if (input.purpose !== 'direct' && input.state !== undefined)
153
152
  fail('binding_conflict');
154
- if (input.purpose === 'direct' && (typeof input.state !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(input.state) || !input.sessionId))
153
+ if (input.purpose === 'direct' && (typeof input.state !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(input.state) || typeof input.sessionKey !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(input.sessionKey)))
155
154
  fail('binding_conflict');
156
- if (input.purpose === 'browser' && !input.sessionId)
155
+ if (input.purpose !== 'direct' && input.sessionKey !== undefined)
157
156
  fail('binding_conflict');
158
- if (input.purpose === 'browser')
159
- text(input.returnTo);
160
157
  const metadata = await verified(text(input.domain), text(input.issuer));
161
- if (input.purpose === 'browser' && (!onDomain(input.returnTo, metadata.document.organization.domain) || !metadata.document.web?.browser_session_endpoint))
162
- fail('binding_conflict');
163
158
  const identity = await store.transaction({ kind: 'identity', enrollmentId: enrollmentId(credential), issuer: input.issuer }, tx => {
164
159
  const enrollment = auth(tx, credential, text(input.installationId));
165
160
  if (!enrollment.allowedCompanies.includes(metadata.document.organization.domain))
166
161
  fail('unauthorized');
167
162
  return { enrollment, userId: user(tx, enrollment.visitor, input.issuer) };
168
163
  });
169
- if (input.purpose === 'browser' && !await options.verifySession?.({ clientId: options.clientId, userId: identity.userId, issuer: input.issuer, sessionId: input.sessionId }))
170
- fail('unauthorized');
171
- const binding = { issuer: input.issuer, domain: metadata.document.organization.domain, purpose: input.purpose, sessionId: input.sessionId ?? null, returnTo: input.returnTo ?? null, stateHash: input.state ? hash(input.state) : null, userId: identity.userId, browserEndpoint: metadata.document.web?.browser_session_endpoint ?? null };
164
+ const binding = { issuer: input.issuer, domain: metadata.document.organization.domain, purpose: input.purpose, sessionKey: input.sessionKey ?? null, sessionId: input.sessionId ?? null, stateHash: input.state ? hash(input.state) : null, userId: identity.userId };
172
165
  return store.transaction({ kind: 'begin', enrollmentId: enrollmentId(credential), operationId: input.operationId, issuer: input.issuer, ...(binding.stateHash ? { stateHash: binding.stateHash } : {}), at: now() }, tx => {
173
166
  const enrollment = auth(tx, credential, text(input.installationId)), key = `${enrollment.enrollmentId}:${text(input.operationId)}`, existing = tx.get('operation', key);
174
167
  if (existing) {
@@ -203,19 +196,16 @@ export function createProvider(options) {
203
196
  exact(input, ['version', 'installationId', 'operationId', 'purpose']);
204
197
  const snapshot = await store.transaction(selectedOperation(credential, input), tx => {
205
198
  const enrollment = auth(tx, credential, text(input.installationId)), op = operation(tx, enrollment, text(input.operationId));
206
- if (op.binding.purpose !== input.purpose || !['client-authentication', 'session', 'browser'].includes(input.purpose))
199
+ if (op.binding.purpose !== input.purpose || !['client-authentication', 'session'].includes(input.purpose))
207
200
  fail('binding_conflict');
208
201
  throttle(tx, enrollment);
209
202
  return { enrollment, op };
210
203
  });
211
- const b = snapshot.op.binding, metadata = await verified(b.domain, b.issuer);
212
- if (b.browserEndpoint !== (metadata.document.web?.browser_session_endpoint ?? null))
213
- fail('binding_conflict');
214
- if (b.purpose === 'browser' && !await options.verifySession?.({ clientId: options.clientId, userId: b.userId, issuer: b.issuer, sessionId: b.sessionId }))
215
- fail('unauthorized');
204
+ const b = snapshot.op.binding;
205
+ await verified(b.domain, b.issuer);
216
206
  const iat = Math.floor(now() / 1000), exp = iat + 60;
217
- const claims = { iss: options.clientId, sub: b.purpose === 'client-authentication' ? options.clientId : b.userId, aud: b.purpose === 'browser' ? b.browserEndpoint : b.issuer, iat, exp, jti: id(), ...(b.purpose === 'browser' ? { session_id: b.sessionId, return_to: b.returnTo } : {}) };
218
- const header = { alg: 'ES256', kid: options.signer.kid, typ: b.purpose === 'client-authentication' ? 'client-authentication+jwt' : b.purpose === 'browser' ? 'poppy-browser+jwt' : 'JWT' };
207
+ const claims = { iss: options.clientId, sub: b.purpose === 'client-authentication' ? options.clientId : b.userId, aud: b.issuer, iat, exp, jti: id() };
208
+ const header = { alg: 'ES256', kid: options.signer.kid, typ: b.purpose === 'client-authentication' ? 'client-authentication+jwt' : 'JWT' };
219
209
  const unsigned = `${Buffer.from(JSON.stringify(header)).toString('base64url')}.${Buffer.from(JSON.stringify(claims)).toString('base64url')}`;
220
210
  const signature = await options.signer.sign(new TextEncoder().encode(unsigned));
221
211
  if (signature.length !== 64)
@@ -252,10 +242,10 @@ export function createProvider(options) {
252
242
  });
253
243
  },
254
244
  async takeResult(credential, input) {
255
- exact(input, ['version', 'installationId', 'operationId', 'issuer', 'sessionId']);
245
+ exact(input, ['version', 'installationId', 'operationId', 'issuer', 'sessionId', 'sessionKey']);
256
246
  return store.transaction(selectedOperation(credential, input), tx => {
257
247
  const enrollment = auth(tx, credential, text(input.installationId)), op = operation(tx, enrollment, text(input.operationId));
258
- if (op.binding.issuer !== input.issuer || op.binding.sessionId !== input.sessionId || op.binding.purpose !== 'direct')
248
+ if (op.binding.issuer !== input.issuer || op.binding.sessionKey !== input.sessionKey || op.binding.purpose !== 'direct')
259
249
  fail('binding_conflict');
260
250
  if (op.status !== 'ready')
261
251
  return { status: 'pending' };
@@ -7,7 +7,7 @@ if (!domain) throw new Error('usage: node examples/quickstart.mjs DOMAIN|--self-
7
7
  const transport = selfCheck ? {
8
8
  async fetch(request) {
9
9
  assert.equal(request.url, 'https://company.example/.well-known/poppy.json');
10
- return Response.json({ protocol_version: '0.2', organization: { name: 'Example Company', domain }, web: {} });
10
+ return Response.json({ protocol_version: '0.3', organization: { name: 'Example Company', domain }, web: {} });
11
11
  },
12
12
  } : createNodeTransport();
13
13
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poppykit/client",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Personal Agent Protocol client SDK",
6
6
  "repository": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "type": "module",
11
11
  "engines": {
12
- "node": ">=24.22.1 <25"
12
+ "node": ">=24.21.0 <25"
13
13
  },
14
14
  "packageManager": "pnpm@11.25.0",
15
15
  "scripts": {