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

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.2",
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: "1f74d4e07311496f0acc021855137bc197d79eab",
43273
43349
  types: "./dist/index.d.ts"
43274
43350
  };
43275
43351
 
@@ -43374,8 +43450,11 @@ 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",
43379
43458
  "@sockethub/platform-xmpp"
43380
43459
  ]
43381
43460
  },
@@ -43391,7 +43470,8 @@ var SockethubConfigSchema = {
43391
43470
  connectTimeoutMs: { type: "integer", minimum: 1 },
43392
43471
  allowPrivateAddresses: { type: "boolean" },
43393
43472
  allowInsecureHttp: { type: "boolean" },
43394
- concurrency: { type: "integer", minimum: 1 }
43473
+ concurrency: { type: "integer", minimum: 1 },
43474
+ discoveryCacheTtlMs: { type: "integer", minimum: 0 }
43395
43475
  }
43396
43476
  },
43397
43477
  "@sockethub/platform-caldav": {
@@ -43401,7 +43481,20 @@ var SockethubConfigSchema = {
43401
43481
  connectTimeoutMs: { type: "integer", minimum: 1 },
43402
43482
  allowPrivateAddresses: { type: "boolean" },
43403
43483
  allowInsecureHttp: { type: "boolean" },
43404
- concurrency: { type: "integer", minimum: 1 }
43484
+ concurrency: { type: "integer", minimum: 1 },
43485
+ discoveryCacheTtlMs: { type: "integer", minimum: 0 }
43486
+ }
43487
+ },
43488
+ "@sockethub/platform-email": {
43489
+ type: "object",
43490
+ additionalProperties: false,
43491
+ properties: {
43492
+ connectTimeoutMs: {
43493
+ type: "integer",
43494
+ minimum: 5000,
43495
+ maximum: 120000
43496
+ },
43497
+ allowPrivateAddresses: { type: "boolean" }
43405
43498
  }
43406
43499
  },
43407
43500
  "@sockethub/platform-dummy": {
@@ -43424,7 +43517,8 @@ var SockethubConfigSchema = {
43424
43517
  type: "object",
43425
43518
  additionalProperties: false,
43426
43519
  properties: {
43427
- connectTimeoutMs: { type: "number" }
43520
+ connectTimeoutMs: { type: "number" },
43521
+ allowPrivateAddresses: { type: "boolean" }
43428
43522
  }
43429
43523
  },
43430
43524
  "@sockethub/platform-xmpp": {
@@ -43534,6 +43628,12 @@ var SockethubConfigSchema = {
43534
43628
  default: 20,
43535
43629
  description: "Maximum persistent instances per client IP, including HTTP actions. 0 disables the cap."
43536
43630
  },
43631
+ maxStartsPerIpPerMinute: {
43632
+ type: "integer",
43633
+ minimum: 0,
43634
+ default: 60,
43635
+ 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."
43636
+ },
43537
43637
  maxPlatformInstances: {
43538
43638
  type: "number",
43539
43639
  minimum: 0,
@@ -43842,8 +43942,10 @@ function splitEscaped(value, separator) {
43842
43942
  result.push(current);
43843
43943
  return result;
43844
43944
  }
43845
- var unescapeText = (value) => value.replace(/\\n/gi, `
43846
- `).replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
43945
+ function unescapeText(value) {
43946
+ return value.replaceAll(/\\([nN,;\\])/g, (_match, character) => character === "n" || character === "N" ? `
43947
+ ` : character);
43948
+ }
43847
43949
  var escapeText = (value) => value.replace(/\r\n?/g, `
43848
43950
  `).replaceAll("\\", "\\\\").replaceAll(`
43849
43951
  `, "\\n").replaceAll(";", "\\;").replaceAll(",", "\\,");
@@ -44079,20 +44181,20 @@ function params(value) {
44079
44181
  }
44080
44182
  function fold(line) {
44081
44183
  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);
44184
+ let start = 0;
44185
+ let end = 0;
44186
+ let octets = 0;
44187
+ for (const char of line) {
44188
+ const size = Buffer.byteLength(char, "utf8");
44189
+ if (octets + size > (chunks.length ? 74 : 75)) {
44190
+ chunks.push(line.slice(start, end));
44191
+ start = end;
44192
+ octets = 0;
44193
+ }
44194
+ octets += size;
44195
+ end += char.length;
44196
+ }
44197
+ chunks.push(line.slice(start));
44096
44198
  return chunks.join(`\r
44097
44199
  `);
44098
44200
  }
@@ -44383,7 +44485,7 @@ class CardDavClient extends DavClient {
44383
44485
  var package_default2 = {
44384
44486
  name: "@sockethub/platform-carddav",
44385
44487
  description: "A Sockethub platform for CardDAV address books and contacts",
44386
- version: "1.0.0-beta.1",
44488
+ version: "1.0.0-beta.2",
44387
44489
  type: "module",
44388
44490
  private: false,
44389
44491
  license: "LGPL-3.0+",
@@ -44412,21 +44514,21 @@ var package_default2 = {
44412
44514
  test: "bun test src/"
44413
44515
  },
44414
44516
  dependencies: {
44415
- "@sockethub/dav": "^1.0.0-beta.1",
44416
- "@sockethub/schemas": "^3.0.0-beta.1"
44517
+ "@sockethub/dav": "^1.0.0-beta.2",
44518
+ "@sockethub/schemas": "^3.0.0-beta.2"
44417
44519
  },
44418
44520
  devDependencies: {
44419
44521
  "@types/bun": "latest"
44420
44522
  },
44421
44523
  peerDependencies: {
44422
- "@sockethub/server": "^5.0.0-beta.1"
44524
+ "@sockethub/server": "^5.0.0-beta.2"
44423
44525
  },
44424
44526
  peerDependenciesMeta: {
44425
44527
  "@sockethub/server": {
44426
44528
  optional: true
44427
44529
  }
44428
44530
  },
44429
- gitHead: "06c9062f1cb5f0c0599fd979e6e91da79f08eda0"
44531
+ gitHead: "1f74d4e07311496f0acc021855137bc197d79eab"
44430
44532
  };
44431
44533
 
44432
44534
  // src/schema.ts
@@ -44719,13 +44821,15 @@ var CONTEXT = buildCanonicalContext(PlatformCardDavSchema.contextUrl);
44719
44821
 
44720
44822
  class CardDav {
44721
44823
  log;
44824
+ discovery = new DiscoveryCache;
44722
44825
  config = {
44723
44826
  persist: false,
44724
44827
  requireCredentials: ["fetch", "query", "create", "update", "delete"],
44725
44828
  connectTimeoutMs: 15000,
44726
44829
  allowPrivateAddresses: false,
44727
44830
  allowInsecureHttp: false,
44728
- concurrency: 10
44831
+ concurrency: 10,
44832
+ discoveryCacheTtlMs: 300000
44729
44833
  };
44730
44834
  constructor(session) {
44731
44835
  this.log = session.log;
@@ -44748,40 +44852,40 @@ class CardDav {
44748
44852
  summary: "CardDAV address books",
44749
44853
  totalItems: items.length,
44750
44854
  items
44751
- })).catch((error) => this.fail(job, error, done)).finally(() => client.close().catch(() => {}));
44855
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44752
44856
  }
44753
44857
  query(job, credentials, done) {
44754
44858
  const client = this.client(credentials);
44755
- this.book(client, job.target?.id ?? "").then((book) => client.query(book, job.object ?? {})).then((items) => done(null, {
44859
+ this.book(client, credentials, job.target?.id ?? "").then((book) => client.query(book, job.object ?? {})).then((items) => done(null, {
44756
44860
  "@context": CONTEXT,
44757
44861
  ...job.id ? { id: job.id } : {},
44758
44862
  type: "collection",
44759
44863
  summary: "CardDAV contacts",
44760
44864
  totalItems: items.length,
44761
44865
  items
44762
- })).catch((error) => this.fail(job, error, done)).finally(() => client.close().catch(() => {}));
44866
+ })).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44763
44867
  }
44764
44868
  create(job, credentials, done) {
44765
44869
  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(() => {}));
44870
+ 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
44871
  }
44768
44872
  update(job, credentials, done) {
44769
44873
  const client = this.client(credentials);
44770
44874
  const input = job.object;
44771
- this.book(client, job.target?.id ?? "").then((book) => {
44875
+ this.book(client, credentials, job.target?.id ?? "").then((book) => {
44772
44876
  if (!isDavCollectionChild(book.id, input.id))
44773
44877
  throw new DavFailure("carddav:invalid-resource");
44774
44878
  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(() => {}));
44879
+ }).then((result) => done(null, this.mutation(job, "update", result))).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44776
44880
  }
44777
44881
  delete(job, credentials, done) {
44778
44882
  const client = this.client(credentials);
44779
44883
  const input = job.object;
44780
- this.book(client, job.target?.id ?? "").then((book) => {
44884
+ this.book(client, credentials, job.target?.id ?? "").then((book) => {
44781
44885
  if (!isDavCollectionChild(book.id, input.id))
44782
44886
  throw new DavFailure("carddav:invalid-resource");
44783
44887
  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(() => {}));
44888
+ }).then(() => done(null, this.mutation(job, "delete", { id: input.id }))).catch((error) => this.fail(job, error, done, credentials)).finally(() => client.close().catch(() => {}));
44785
44889
  }
44786
44890
  client(credentials) {
44787
44891
  const { url, ...authentication } = credentials.object;
@@ -44790,14 +44894,18 @@ class CardDav {
44790
44894
  allowInsecureHttp: this.config.allowInsecureHttp
44791
44895
  });
44792
44896
  }
44793
- async book(client, id) {
44897
+ discoveryKey(credentials) {
44898
+ return this.discovery.key("carddav", credentials.object);
44899
+ }
44900
+ async book(client, credentials, id) {
44794
44901
  let normalized;
44795
44902
  try {
44796
44903
  normalized = new URL(id).href;
44797
44904
  } catch {
44798
44905
  throw new DavFailure("carddav:invalid-address-book");
44799
44906
  }
44800
- const book = (await client.discoverAddressBooks()).find((item) => item.id === normalized);
44907
+ const books = await this.discovery.get(this.discoveryKey(credentials), this.config.discoveryCacheTtlMs, () => client.discoverAddressBooks());
44908
+ const book = books.find((item) => item.id === normalized);
44801
44909
  if (!book)
44802
44910
  throw new DavFailure("carddav:invalid-address-book");
44803
44911
  return book;
@@ -44817,7 +44925,9 @@ class CardDav {
44817
44925
  }
44818
44926
  };
44819
44927
  }
44820
- fail(job, error, done) {
44928
+ fail(job, error, done, credentials) {
44929
+ if (credentials && error instanceof DavFailure && /:(authentication-failed|not-found|invalid-address-book)$/.test(error.code))
44930
+ this.discovery.delete(this.discoveryKey(credentials));
44821
44931
  const code = error instanceof DavFailure ? error.code : `carddav:invalid-${job.type}`;
44822
44932
  this.log.error(`CardDAV ${job.type} failed for actor ${job.actor.id}`, {
44823
44933
  code,
@@ -44835,4 +44945,4 @@ export {
44835
44945
  parseVCard
44836
44946
  };
44837
44947
 
44838
- //# debugId=994C753F4E4E8EBA64756E2164756E21
44948
+ //# debugId=DCD67AD60F21618364756E2164756E21