@sockethub/platform-carddav 1.0.0-beta.1 → 1.0.0-beta.3

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/README.md CHANGED
@@ -12,6 +12,19 @@ including IPv6 embeddings of private ranges and the local-use NAT64 prefix
12
12
  Authentication supports Basic, Digest, or a caller-provided Bearer token
13
13
  through the standard Sockethub credentials flow.
14
14
 
15
+ ## Discovery cache
16
+
17
+ Each `query`, `create`, `update`, and `delete` needs the account's address book list to
18
+ check the target. The platform caches that discovery result in memory for
19
+ `packageConfig.discoveryCacheTtlMs` (default `300000`, 5 minutes; `0`
20
+ disables the cache). Entries are keyed by an HMAC of the protocol, service URL,
21
+ and credentials. The HMAC uses a per-process random secret, and only
22
+ collection metadata is stored. A `fetch` always discovers fresh. Within the TTL,
23
+ an address book added on the server is not visible to targeted actions, and a removed
24
+ one is still accepted as a target (the server then rejects the request). An
25
+ authentication, not-found, or unknown-address-book error drops the entry, so the next
26
+ request rediscovers. The cache is lost when the platform process restarts.
27
+
15
28
  ## Authentication
16
29
 
17
30
  Send credentials before performing CardDAV actions. Use either a username and
package/dist/index.d.ts CHANGED
@@ -2,7 +2,10 @@ import type { ActivityStream, PlatformCallback, PlatformInterface, PlatformSchem
2
2
  import type { CardDavCredentials } from "./types.js";
3
3
  export default class CardDav implements PlatformInterface {
4
4
  private readonly log;
5
- config: StatelessPlatformConfig;
5
+ private readonly discovery;
6
+ config: StatelessPlatformConfig & {
7
+ discoveryCacheTtlMs: number;
8
+ };
6
9
  constructor(session: PlatformSession);
7
10
  get schema(): PlatformSchemaStruct;
8
11
  isInitialized(): boolean;
@@ -13,6 +16,7 @@ export default class CardDav implements PlatformInterface {
13
16
  update(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
14
17
  delete(job: ActivityStream, credentials: CardDavCredentials, done: PlatformCallback): void;
15
18
  private client;
19
+ private discoveryKey;
16
20
  private book;
17
21
  private mutation;
18
22
  private fail;
package/dist/index.js CHANGED
@@ -15762,7 +15762,7 @@ var require_ajv_formats_draft2019 = __commonJS(function(exports, module) {
15762
15762
 
15763
15763
  // ../dav/dist/index.js
15764
15764
  import { createRequire as createRequire2 } from "node:module";
15765
- import { createHash, randomBytes } from "node:crypto";
15765
+ import { createHash, createHmac, randomBytes } from "node:crypto";
15766
15766
  import { lookup as dnsLookup } from "node:dns";
15767
15767
  import { isIP } from "node:net";
15768
15768
  var __commonJS2 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -39185,7 +39185,7 @@ function createGuardedLookup(allowPrivateAddresses) {
39185
39185
  if (!allowPrivateAddresses) {
39186
39186
  for (const { address } of addresses) {
39187
39187
  if (isBlockedAddress(address)) {
39188
- callback(new Error(`blocked non-public address ${address} for ${hostname}`));
39188
+ callback(new Error(`blocked non-public address for ${hostname}`));
39189
39189
  return;
39190
39190
  }
39191
39191
  }
@@ -42746,6 +42746,82 @@ function isDavCollectionChild(collectionId, resourceId) {
42746
42746
  return false;
42747
42747
  }
42748
42748
  }
42749
+
42750
+ class DiscoveryCache {
42751
+ maxEntries;
42752
+ secret = randomBytes(32);
42753
+ entries = new Map;
42754
+ pending = new Map;
42755
+ timer;
42756
+ timerAt = Number.POSITIVE_INFINITY;
42757
+ constructor(maxEntries = 1000) {
42758
+ this.maxEntries = maxEntries;
42759
+ }
42760
+ key(...identity) {
42761
+ return createHmac("sha256", this.secret).update(JSON.stringify(identity)).digest("hex");
42762
+ }
42763
+ async get(key, ttlMs, discover) {
42764
+ if (!(ttlMs > 0))
42765
+ return discover();
42766
+ const hit = this.entries.get(key);
42767
+ if (hit && hit.expires > Date.now())
42768
+ return hit.value;
42769
+ const inFlight = this.pending.get(key);
42770
+ if (inFlight)
42771
+ return inFlight;
42772
+ const promise = discover();
42773
+ this.pending.set(key, promise);
42774
+ try {
42775
+ const value = await promise;
42776
+ if (this.pending.get(key) === promise)
42777
+ this.store(key, value, Date.now() + ttlMs);
42778
+ return value;
42779
+ } finally {
42780
+ if (this.pending.get(key) === promise)
42781
+ this.pending.delete(key);
42782
+ }
42783
+ }
42784
+ delete(key) {
42785
+ this.entries.delete(key);
42786
+ this.pending.delete(key);
42787
+ }
42788
+ get size() {
42789
+ return this.entries.size;
42790
+ }
42791
+ store(key, value, expires) {
42792
+ this.entries.delete(key);
42793
+ if (this.entries.size >= this.maxEntries) {
42794
+ this.sweep();
42795
+ if (this.entries.size >= this.maxEntries)
42796
+ this.entries.delete(this.entries.keys().next().value);
42797
+ }
42798
+ this.entries.set(key, { expires, value });
42799
+ this.schedule(expires);
42800
+ }
42801
+ sweep() {
42802
+ const now = Date.now();
42803
+ for (const [key, entry] of this.entries)
42804
+ if (entry.expires <= now)
42805
+ this.entries.delete(key);
42806
+ }
42807
+ schedule(at) {
42808
+ if (this.timer && at >= this.timerAt)
42809
+ return;
42810
+ clearTimeout(this.timer);
42811
+ this.timerAt = at;
42812
+ this.timer = setTimeout(() => {
42813
+ this.timer = undefined;
42814
+ this.timerAt = Number.POSITIVE_INFINITY;
42815
+ this.sweep();
42816
+ let next = Number.POSITIVE_INFINITY;
42817
+ for (const entry of this.entries.values())
42818
+ next = Math.min(next, entry.expires);
42819
+ if (next !== Number.POSITIVE_INFINITY)
42820
+ this.schedule(next);
42821
+ }, Math.min(Math.max(0, at - Date.now()), 2147483647));
42822
+ this.timer.unref?.();
42823
+ }
42824
+ }
42749
42825
  function unquote(value) {
42750
42826
  return value.startsWith('"') ? value.slice(1, -1).replace(/\\(.)/g, "$1") : value;
42751
42827
  }
@@ -43199,7 +43275,7 @@ var ActivityStreamSchema = {
43199
43275
  var package_default = {
43200
43276
  name: "@sockethub/schemas",
43201
43277
  description: "JSON schema files for validating Sockethub Activity Streams",
43202
- version: "3.0.0-beta.1",
43278
+ version: "3.0.0-beta.3",
43203
43279
  type: "module",
43204
43280
  private: false,
43205
43281
  author: "Nick Jennings <nick@silverbucket.net>",
@@ -43269,7 +43345,7 @@ var package_default = {
43269
43345
  "ajv-formats": "^2.1.1",
43270
43346
  "ajv-formats-draft2019": "^1.6.1"
43271
43347
  },
43272
- gitHead: "06c9062f1cb5f0c0599fd979e6e91da79f08eda0",
43348
+ gitHead: "bc04522d60d1e2fa1b3098b339897de62c3b0549",
43273
43349
  types: "./dist/index.d.ts"
43274
43350
  };
43275
43351
 
@@ -43374,8 +43450,12 @@ var SockethubConfigSchema = {
43374
43450
  "@sockethub/platform-caldav",
43375
43451
  "@sockethub/platform-dummy",
43376
43452
  "@sockethub/platform-feeds",
43453
+ "@sockethub/platform-email",
43377
43454
  "@sockethub/platform-irc",
43378
43455
  "@sockethub/platform-metadata",
43456
+ "@sockethub/platform-messenger",
43457
+ "@sockethub/platform-instagram",
43458
+ "@sockethub/platform-whatsapp",
43379
43459
  "@sockethub/platform-xmpp"
43380
43460
  ]
43381
43461
  },
@@ -43391,7 +43471,8 @@ var SockethubConfigSchema = {
43391
43471
  connectTimeoutMs: { type: "integer", minimum: 1 },
43392
43472
  allowPrivateAddresses: { type: "boolean" },
43393
43473
  allowInsecureHttp: { type: "boolean" },
43394
- concurrency: { type: "integer", minimum: 1 }
43474
+ concurrency: { type: "integer", minimum: 1 },
43475
+ discoveryCacheTtlMs: { type: "integer", minimum: 0 }
43395
43476
  }
43396
43477
  },
43397
43478
  "@sockethub/platform-caldav": {
@@ -43401,7 +43482,20 @@ var SockethubConfigSchema = {
43401
43482
  connectTimeoutMs: { type: "integer", minimum: 1 },
43402
43483
  allowPrivateAddresses: { type: "boolean" },
43403
43484
  allowInsecureHttp: { type: "boolean" },
43404
- concurrency: { type: "integer", minimum: 1 }
43485
+ concurrency: { type: "integer", minimum: 1 },
43486
+ discoveryCacheTtlMs: { type: "integer", minimum: 0 }
43487
+ }
43488
+ },
43489
+ "@sockethub/platform-email": {
43490
+ type: "object",
43491
+ additionalProperties: false,
43492
+ properties: {
43493
+ connectTimeoutMs: {
43494
+ type: "integer",
43495
+ minimum: 5000,
43496
+ maximum: 120000
43497
+ },
43498
+ allowPrivateAddresses: { type: "boolean" }
43405
43499
  }
43406
43500
  },
43407
43501
  "@sockethub/platform-dummy": {
@@ -43424,7 +43518,8 @@ var SockethubConfigSchema = {
43424
43518
  type: "object",
43425
43519
  additionalProperties: false,
43426
43520
  properties: {
43427
- connectTimeoutMs: { type: "number" }
43521
+ connectTimeoutMs: { type: "number" },
43522
+ allowPrivateAddresses: { type: "boolean" }
43428
43523
  }
43429
43524
  },
43430
43525
  "@sockethub/platform-xmpp": {
@@ -43534,6 +43629,12 @@ var SockethubConfigSchema = {
43534
43629
  default: 20,
43535
43630
  description: "Maximum persistent instances per client IP, including HTTP actions. 0 disables the cap."
43536
43631
  },
43632
+ maxStartsPerIpPerMinute: {
43633
+ type: "integer",
43634
+ minimum: 0,
43635
+ default: 60,
43636
+ description: "Maximum new persistent instances a client IP may start per minute, " + "including HTTP actions. Bounds fork/teardown churn that the " + "concurrent caps allow. Reusing a running instance does not count. " + "0 disables the cap."
43637
+ },
43537
43638
  maxPlatformInstances: {
43538
43639
  type: "number",
43539
43640
  minimum: 0,
@@ -43842,8 +43943,10 @@ function splitEscaped(value, separator) {
43842
43943
  result.push(current);
43843
43944
  return result;
43844
43945
  }
43845
- var unescapeText = (value) => value.replace(/\\n/gi, `
43846
- `).replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
43946
+ function unescapeText(value) {
43947
+ return value.replaceAll(/\\([nN,;\\])/g, (_match, character) => character === "n" || character === "N" ? `
43948
+ ` : character);
43949
+ }
43847
43950
  var escapeText = (value) => value.replace(/\r\n?/g, `
43848
43951
  `).replaceAll("\\", "\\\\").replaceAll(`
43849
43952
  `, "\\n").replaceAll(";", "\\;").replaceAll(",", "\\,");
@@ -44079,20 +44182,20 @@ function params(value) {
44079
44182
  }
44080
44183
  function fold(line) {
44081
44184
  const chunks = [];
44082
- let rest = line;
44083
- while (Buffer.byteLength(rest, "utf8") > 75) {
44084
- let end = Math.min(75, rest.length);
44085
- while (Buffer.byteLength(rest.slice(0, end), "utf8") > 75)
44086
- end -= 1;
44087
- const preceding = rest.charCodeAt(end - 1);
44088
- if (preceding >= 55296 && preceding <= 56319)
44089
- end -= 1;
44090
- if (end <= 0)
44091
- throw new Error("unable to fold vCard content line");
44092
- chunks.push(rest.slice(0, end));
44093
- rest = rest.slice(end);
44094
- }
44095
- chunks.push(rest);
44185
+ let start = 0;
44186
+ let end = 0;
44187
+ let octets = 0;
44188
+ for (const char of line) {
44189
+ const size = Buffer.byteLength(char, "utf8");
44190
+ if (octets + size > (chunks.length ? 74 : 75)) {
44191
+ chunks.push(line.slice(start, end));
44192
+ start = end;
44193
+ octets = 0;
44194
+ }
44195
+ octets += size;
44196
+ end += char.length;
44197
+ }
44198
+ chunks.push(line.slice(start));
44096
44199
  return chunks.join(`\r
44097
44200
  `);
44098
44201
  }
@@ -44383,7 +44486,7 @@ class CardDavClient extends DavClient {
44383
44486
  var package_default2 = {
44384
44487
  name: "@sockethub/platform-carddav",
44385
44488
  description: "A Sockethub platform for CardDAV address books and contacts",
44386
- version: "1.0.0-beta.1",
44489
+ version: "1.0.0-beta.3",
44387
44490
  type: "module",
44388
44491
  private: false,
44389
44492
  license: "LGPL-3.0+",
@@ -44412,21 +44515,21 @@ var package_default2 = {
44412
44515
  test: "bun test src/"
44413
44516
  },
44414
44517
  dependencies: {
44415
- "@sockethub/dav": "^1.0.0-beta.1",
44416
- "@sockethub/schemas": "^3.0.0-beta.1"
44518
+ "@sockethub/dav": "^1.0.0-beta.3",
44519
+ "@sockethub/schemas": "^3.0.0-beta.3"
44417
44520
  },
44418
44521
  devDependencies: {
44419
44522
  "@types/bun": "latest"
44420
44523
  },
44421
44524
  peerDependencies: {
44422
- "@sockethub/server": "^5.0.0-beta.1"
44525
+ "@sockethub/server": "^5.0.0-beta.3"
44423
44526
  },
44424
44527
  peerDependenciesMeta: {
44425
44528
  "@sockethub/server": {
44426
44529
  optional: true
44427
44530
  }
44428
44531
  },
44429
- gitHead: "06c9062f1cb5f0c0599fd979e6e91da79f08eda0"
44532
+ gitHead: "bc04522d60d1e2fa1b3098b339897de62c3b0549"
44430
44533
  };
44431
44534
 
44432
44535
  // src/schema.ts
@@ -44719,13 +44822,15 @@ var CONTEXT = buildCanonicalContext(PlatformCardDavSchema.contextUrl);
44719
44822
 
44720
44823
  class CardDav {
44721
44824
  log;
44825
+ discovery = new DiscoveryCache;
44722
44826
  config = {
44723
44827
  persist: false,
44724
44828
  requireCredentials: ["fetch", "query", "create", "update", "delete"],
44725
44829
  connectTimeoutMs: 15000,
44726
44830
  allowPrivateAddresses: false,
44727
44831
  allowInsecureHttp: false,
44728
- concurrency: 10
44832
+ concurrency: 10,
44833
+ discoveryCacheTtlMs: 300000
44729
44834
  };
44730
44835
  constructor(session) {
44731
44836
  this.log = session.log;
@@ -44748,40 +44853,40 @@ class CardDav {
44748
44853
  summary: "CardDAV address books",
44749
44854
  totalItems: items.length,
44750
44855
  items
44751
- })).catch((error) => this.fail(job, error, done)).finally(() => client.close().catch(() => {}));
44856
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44752
44857
  }
44753
44858
  query(job, credentials, done) {
44754
44859
  const client = this.client(credentials);
44755
- this.book(client, job.target?.id ?? "").then((book) => client.query(book, job.object ?? {})).then((items) => done(null, {
44860
+ this.book(client, credentials, job.target?.id ?? "").then((book) => client.query(book, job.object ?? {})).then((items) => done(null, {
44756
44861
  "@context": CONTEXT,
44757
44862
  ...job.id ? { id: job.id } : {},
44758
44863
  type: "collection",
44759
44864
  summary: "CardDAV contacts",
44760
44865
  totalItems: items.length,
44761
44866
  items
44762
- })).catch((error) => this.fail(job, error, done)).finally(() => client.close().catch(() => {}));
44867
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44763
44868
  }
44764
44869
  create(job, credentials, done) {
44765
44870
  const client = this.client(credentials);
44766
- this.book(client, job.target?.id ?? "").then((book) => client.create(book, job.object)).then((result) => done(null, this.mutation(job, "create", result))).catch((error) => this.fail(job, error, done)).finally(() => client.close().catch(() => {}));
44871
+ this.book(client, credentials, job.target?.id ?? "").then((book) => client.create(book, job.object)).then((result) => done(null, this.mutation(job, "create", result))).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44767
44872
  }
44768
44873
  update(job, credentials, done) {
44769
44874
  const client = this.client(credentials);
44770
44875
  const input = job.object;
44771
- this.book(client, job.target?.id ?? "").then((book) => {
44876
+ this.book(client, credentials, job.target?.id ?? "").then((book) => {
44772
44877
  if (!isDavCollectionChild(book.id, input.id))
44773
44878
  throw new DavFailure("carddav:invalid-resource");
44774
44879
  return client.update(input);
44775
- }).then((result) => done(null, this.mutation(job, "update", result))).catch((error) => this.fail(job, error, done)).finally(() => client.close().catch(() => {}));
44880
+ }).then((result) => done(null, this.mutation(job, "update", result))).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44776
44881
  }
44777
44882
  delete(job, credentials, done) {
44778
44883
  const client = this.client(credentials);
44779
44884
  const input = job.object;
44780
- this.book(client, job.target?.id ?? "").then((book) => {
44885
+ this.book(client, credentials, job.target?.id ?? "").then((book) => {
44781
44886
  if (!isDavCollectionChild(book.id, input.id))
44782
44887
  throw new DavFailure("carddav:invalid-resource");
44783
44888
  return client.delete(input.id, input.etag);
44784
- }).then(() => done(null, this.mutation(job, "delete", { id: input.id }))).catch((error) => this.fail(job, error, done)).finally(() => client.close().catch(() => {}));
44889
+ }).then(() => done(null, this.mutation(job, "delete", { id: input.id }))).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44785
44890
  }
44786
44891
  client(credentials) {
44787
44892
  const { url, ...authentication } = credentials.object;
@@ -44790,14 +44895,18 @@ class CardDav {
44790
44895
  allowInsecureHttp: this.config.allowInsecureHttp
44791
44896
  });
44792
44897
  }
44793
- async book(client, id) {
44898
+ discoveryKey(credentials) {
44899
+ return this.discovery.key("carddav", credentials.object);
44900
+ }
44901
+ async book(client, credentials, id) {
44794
44902
  let normalized;
44795
44903
  try {
44796
44904
  normalized = new URL(id).href;
44797
44905
  } catch {
44798
44906
  throw new DavFailure("carddav:invalid-address-book");
44799
44907
  }
44800
- const book = (await client.discoverAddressBooks()).find((item) => item.id === normalized);
44908
+ const books = await this.discovery.get(this.discoveryKey(credentials), this.config.discoveryCacheTtlMs, () => client.discoverAddressBooks());
44909
+ const book = books.find((item) => item.id === normalized);
44801
44910
  if (!book)
44802
44911
  throw new DavFailure("carddav:invalid-address-book");
44803
44912
  return book;
@@ -44817,7 +44926,9 @@ class CardDav {
44817
44926
  }
44818
44927
  };
44819
44928
  }
44820
- fail(job, error, done) {
44929
+ fail(job, error, done, credentials) {
44930
+ if (credentials && error instanceof DavFailure && /:(authentication-failed|not-found|invalid-address-book)$/.test(error.code))
44931
+ this.discovery.delete(this.discoveryKey(credentials));
44821
44932
  const code = error instanceof DavFailure ? error.code : `carddav:invalid-${job.type}`;
44822
44933
  this.log.error(`CardDAV ${job.type} failed for actor ${job.actor.id}`, {
44823
44934
  code,
@@ -44835,4 +44946,4 @@ export {
44835
44946
  parseVCard
44836
44947
  };
44837
44948
 
44838
- //# debugId=994C753F4E4E8EBA64756E2164756E21
44949
+ //# debugId=D20292E281EC330764756E2164756E21