@aiam/ciba 0.8.6 → 0.8.8

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 (2) hide show
  1. package/ciba.mjs +49 -51
  2. package/package.json +1 -1
package/ciba.mjs CHANGED
@@ -438,73 +438,71 @@ function startDaemon(provider, deviceDoc, privateKey, serverUrl) {
438
438
  dlog(`${req.command} cmd attrs=${JSON.stringify(req.attrs)}`);
439
439
  const requests = deviceDoc.getMap('requests');
440
440
  const resourcesMap = deviceDoc.getMap('resources');
441
-
442
- // Pass attrs straight through — server resolves grant_type from resource URN.
443
- const attrs = { ...(req.attrs || {}) };
444
441
  const defaultResource = `urn:sap:destination:${process.env.CIBA_DEFAULT_DESTINATION || 'WEBAGENTS_BACKEND'}`;
442
+ const attrs = { ...(req.attrs || {}) };
445
443
  const requestedResource = attrs.resource ?? defaultResource;
446
444
 
447
- const decryptFromTokenMap = (tokenMap) => {
448
- const ciphertext = tokenMap.get('ciphertext');
449
- const serverPublicKey = tokenMap.get('serverPublicKey');
450
- if (!ciphertext || !serverPublicKey) return null;
451
- const plain = ecdhDecrypt(ciphertext, serverPublicKey, privateKey);
452
- try { const parsed = JSON.parse(plain); return parsed.access_token ?? plain; }
445
+ // Pure: check resources map right now, return token map or null.
446
+ function tryGet(resource) {
447
+ const key = resource
448
+ ? resourcesMap.get(resource)
449
+ : [...resourcesMap.values()][0];
450
+ if (!key) return null;
451
+ const map = deviceDoc.getMap(key);
452
+ const expiresAt = map.get('expires_at');
453
+ if (expiresAt && expiresAt < Date.now()) return null; // expired
454
+ if (!map.get('ciphertext')) return null;
455
+ return map;
456
+ }
457
+
458
+ // Pure: decrypt token map → access_token string.
459
+ function decrypt(map) {
460
+ const plain = ecdhDecrypt(map.get('ciphertext'), map.get('serverPublicKey'), privateKey);
461
+ try { return JSON.parse(plain).access_token ?? plain; }
453
462
  catch { return plain; }
454
- };
463
+ }
455
464
 
456
465
  if (req.command === 'refresh') {
457
- // Fire-and-forget — just set default resource if none given.
466
+ // Fire-and-forget — write request, server exchanges async.
458
467
  if (!attrs.resource) attrs.resource = defaultResource;
459
468
  const newRid = randomBytes(8).toString('base64url');
460
- dlog(`refresh; writing requests[${newRid}] attrs=${JSON.stringify(attrs)}`);
469
+ dlog(`refresh; writing requests[${newRid}] resource=${attrs.resource}`);
461
470
  requests.set(newRid, { ...attrs, status: 'pending', created_at: new Date().toISOString() });
462
471
  conn.end(JSON.stringify({ ok: true }));
463
472
  return;
464
473
  }
465
474
 
466
- // token: cache-first, then wait for server response
467
- const cachedName = resourcesMap.get(requestedResource);
468
- if (cachedName) {
469
- const cachedMap = deviceDoc.getMap(cachedName);
470
- const exp = cachedMap.get('exp');
471
- if (exp && exp > Math.floor(Date.now() / 1000) + 60) {
472
- const token = decryptFromTokenMap(cachedMap);
473
- if (token) { conn.end(JSON.stringify({ token })); return; }
474
- }
475
- }
475
+ // token: observe resources map, resolve when token arrives.
476
+ const waitForResource = (resource) => new Promise((resolve, reject) => {
477
+ const found = tryGet(resource);
478
+ if (found) return resolve(found);
476
479
 
477
- const newRid = randomBytes(8).toString('base64url');
478
- dlog(`cache miss; writing requests[${newRid}] attrs=${JSON.stringify(attrs)}`);
479
- requests.set(newRid, { ...attrs, status: 'pending', created_at: new Date().toISOString() });
480
-
481
- dlog(`resources map: ${JSON.stringify([...resourcesMap.entries()])} requested=${requestedResource}`);
482
- const prevTokenMapName = resourcesMap.get(requestedResource);
483
- const newTokenMap = deviceDoc.getMap(`token:${newRid}`);
484
-
485
- const viaRid = firstInYMap(newTokenMap, (key) => key === 'ciphertext' || key === 'error', 30_000)
486
- .then(() => newTokenMap);
487
-
488
- const viaResources = firstInYMap(
489
- resourcesMap,
490
- (key, val) => key === requestedResource && val && val !== prevTokenMapName,
491
- 30_000
492
- ).then((newMapName) => {
493
- const m = deviceDoc.getMap(newMapName);
494
- return firstInYMap(m, (key) => key === 'ciphertext' || key === 'error', 5_000)
495
- .then(() => m).catch(() => m);
480
+ // Cache miss — write request to trigger server exchange.
481
+ const newRid = randomBytes(8).toString('base64url');
482
+ dlog(`cache miss; writing requests[${newRid}] resource=${resource}`);
483
+ requests.set(newRid, { ...attrs, resource, status: 'pending', created_at: new Date().toISOString() });
484
+
485
+ const timer = setTimeout(() => {
486
+ resourcesMap.unobserve(observer);
487
+ reject(new Error('Timeout waiting for token'));
488
+ }, 30_000);
489
+
490
+ const observer = () => {
491
+ const found = tryGet(resource);
492
+ if (found) {
493
+ clearTimeout(timer);
494
+ resourcesMap.unobserve(observer);
495
+ resolve(found);
496
+ }
497
+ };
498
+ resourcesMap.observe(observer);
496
499
  });
497
500
 
498
- Promise.race([
499
- viaRid.then((map) => ({ src: 'rid', map })),
500
- viaResources.then((map) => ({ src: 'resources', map })),
501
- ])
502
- .then(({ src, map }) => {
503
- dlog(`token resolved via ${src} for ${requestedResource}`);
504
- const err = map.get('error');
505
- if (err) { conn.end(JSON.stringify({ error: err })); return; }
506
- try { conn.end(JSON.stringify({ token: decryptFromTokenMap(map) })); }
507
- catch (e) { conn.end(JSON.stringify({ error: `decrypt failed: ${e.message}` })); }
501
+ waitForResource(requestedResource)
502
+ .then((map) => {
503
+ dlog(`token resolved for ${requestedResource}`);
504
+ const access_token = decrypt(map);
505
+ conn.end(JSON.stringify({ token: access_token }));
508
506
  })
509
507
  .catch((e) => conn.end(JSON.stringify({ error: e.message || 'Timeout waiting for token' })));
510
508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiam/ciba",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "OAuth 2.0 Device Authorization Grant CLI with cross-device push approval (Yjs sync, ECDH-encrypted token delivery, persistent device id)",
5
5
  "type": "module",
6
6
  "bin": {