clavarium-tether 0.1.0-next.13 → 0.1.0-next.15

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.
@@ -65,10 +65,18 @@ export function contacts({ fetch: fetchImpl = globalThis.fetch } = {}) {
65
65
 
66
66
  async function googleList(target, ctx) {
67
67
  const base = CARDDAV(userOf(target));
68
+ const basePath = (() => { try { return new URL(base).pathname.replace(/\/+$/, ''); } catch { return ''; } })();
68
69
  const listed = await req(target, ctx, 'PROPFIND', base, { headers: { Depth: '1', 'Content-Type': 'application/xml; charset=utf-8' }, body: PROPFIND });
69
70
  if (!listed.ok) no(`${target.title}: the address book could not be read (${listed.status})`);
70
- /* The collection lists itself and its contacts; keep the vCard entries. */
71
- const hrefs = hrefsIn(await listed.text()).filter((h) => /\.vcf(\?|$)/i.test(h)).slice(0, LIST_MAX);
71
+ /* The collection lists itself and its contacts; keep the entries, whatever
72
+ they are named (Google uses an id, not a .vcf suffix). A contact is any
73
+ href under the collection that is not the collection itself. */
74
+ const hrefs = hrefsIn(await listed.text()).filter((h) => {
75
+ const clean = h.replace(/[?#].*$/, '');
76
+ if (!clean || clean.endsWith('/')) return false;
77
+ let p; try { p = new URL(h, base).pathname.replace(/\/+$/, ''); } catch { p = clean.replace(/\/+$/, ''); }
78
+ return p !== basePath;
79
+ }).slice(0, LIST_MAX);
72
80
  if (!hrefs.length) return [];
73
81
  const body = `<?xml version="1.0" encoding="utf-8"?><C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav"><D:prop><D:getetag/><C:address-data/></D:prop>${hrefs.map((h) => `<D:href>${h.replace(/&/g, '&amp;').replace(/</g, '&lt;')}</D:href>`).join('')}</C:addressbook-multiget>`;
74
82
  const res = await req(target, ctx, 'REPORT', base, { headers: { Depth: '1', 'Content-Type': 'application/xml; charset=utf-8' }, body });
package/host/oauth.mjs CHANGED
@@ -35,11 +35,13 @@ const b64url = (buf) => Buffer.from(buf).toString('base64url');
35
35
  field, kept by that name for its callers). WHICH client matters: a client is
36
36
  only granted the scopes its app was verified for. For GOOGLE we borrow GNOME
37
37
  Online Accounts' client, verified for mail AND calendar AND contacts, so one
38
- consent gives all three. For MICROSOFT we use Thunderbird's client, verified
39
- for mail (IMAP) only; Microsoft calendar/contacts go over Graph and need a
40
- Graph-verified client. `consentExtra` is what a provider needs on the consent
41
- URL to return a refresh token; `offlineScope` is the same thing when it is a
42
- scope rather than a parameter (Microsoft). */
38
+ consent gives all three. For MICROSOFT we use Thunderbird's own client; both
39
+ are multitenant public clients that take any delegated scope the person
40
+ consents to, and both registered the loopback 127.0.0.1 redirect (any port), so
41
+ mail, calendar and contacts go over Graph on the same copy-pastable address.
42
+ `consentExtra` is what a provider needs on the consent URL to return a refresh
43
+ token; `offlineScope` is the same thing when it is a scope rather than a
44
+ parameter (Microsoft). */
43
45
  export const PROVIDERS = {
44
46
  google: {
45
47
  label: 'Google',
@@ -68,17 +70,21 @@ export const PROVIDERS = {
68
70
  pkce: true,
69
71
  consentExtra: {},
70
72
  offlineScope: 'offline_access',
71
- /* Microsoft over Graph: GNOME Online Accounts' client is a Graph client,
72
- so mail, calendar and contacts all go through graph.microsoft.com (the
73
- mail connector uses its Graph path for Microsoft). mail.send is needed
74
- to send. GOA's client is verified for all of these. */
73
+ /* Microsoft over Graph: mail, calendar and contacts all go through
74
+ graph.microsoft.com (the mail connector uses its Graph path for
75
+ Microsoft). mail.send is needed to send. Thunderbird's public client
76
+ takes these delegated scopes by dynamic consent. */
75
77
  scopes: {
76
78
  mail: 'mail.readwrite mail.send',
77
79
  calendar: 'calendars.readwrite',
78
80
  contacts: 'contacts.readwrite',
79
81
  },
82
+ /* Thunderbird's built-in Microsoft client (open source, public, no secret;
83
+ comm-central OAuth2Providers.sys.mjs). It registered the loopback
84
+ 127.0.0.1 redirect on ANY port, so we reuse the same http://127.0.0.1
85
+ address as Google. */
80
86
  thunderbird: {
81
- clientId: 'b155a604-3c31-4079-93b0-6bb6aa9d5464',
87
+ clientId: '9e5f94bc-e8a4-4e73-b8be-63364c29d753',
82
88
  clientSecret: null,
83
89
  },
84
90
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clavarium-tether",
3
- "version": "0.1.0-next.13",
3
+ "version": "0.1.0-next.15",
4
4
  "description": "Clavarium Tether: an AI agent acts outside its own machine on a tether: levels per target, a person's approval, a journal and a stop (a command and an MCP server for agents, a keeper and a screen for the application). One dependency: ssh2, for the SSH connector.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/clavarium/clavarium-tether#readme",
package/server/name.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  export const NAME = 'tether';
7
7
  export const PRODUCT = 'Clavarium Tether';
8
8
  export const PACKAGE = `clavarium-${NAME}`;
9
- export const VERSION = '0.1.0-next.13';
9
+ export const VERSION = '0.1.0-next.15';
10
10
  /* The agent's folder: where its asks and the keeper's answers are written. */
11
11
  export const FOLDER = `.${NAME}`;
12
12
  /* $TETHER_DIR: the agent's folder; $TETHER_STORE: the keeper's store (serve). */