@agentunion/fastaun 0.5.33 → 0.5.35

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 (113) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +28 -26
  3. package/_packed_docs/CHANGELOG.md +59 -2
  4. package/_packed_docs/INDEX.md +26 -12
  5. package/_packed_docs/KITE_DOCS_GUIDE.md +17 -11
  6. package/_packed_docs/agent.md/SCHEMA.md +1 -1
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +2 -2
  8. package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +58 -8
  9. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +17 -17
  10. package/_packed_docs/protocol/echo-probe-gateway.md +99 -0
  11. package/_packed_docs/protocol/echo-probe-sdk.md +91 -0
  12. package/_packed_docs/sdk/0.5.33-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +464 -316
  13. package/_packed_docs/sdk/0.5.33-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +499 -304
  14. package/_packed_docs/sdk/0.5.34-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +133 -0
  15. package/_packed_docs/sdk/0.5.34-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +107 -0
  16. package/_packed_docs/sdk/0.5.35-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +189 -0
  17. package/_packed_docs/sdk/0.5.35-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +204 -0
  18. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -13
  19. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +13 -1
  20. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +19 -9
  21. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +87 -20
  22. package/_packed_docs/sdk/09-collab-rpc-manual.md +48 -30
  23. package/_packed_docs/sdk/09-custody-api-manual.md +3 -3
  24. package/_packed_docs/sdk/09-group-rpc-manual.md +60 -13
  25. package/_packed_docs/sdk/09-message-rpc-manual.md +22 -0
  26. package/_packed_docs/sdk/09-meta-rpc-manual.md +2 -1
  27. package/_packed_docs/sdk/09-payload-reference.md +9 -8
  28. package/_packed_docs/sdk/09-proxy-rpc-manual.md +20 -14
  29. package/_packed_docs/sdk/09-storage-rpc-manual.md +1698 -1659
  30. package/_packed_docs/sdk/09-stream-rpc-manual.md +33 -20
  31. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
  32. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +125 -30
  33. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/274/200/345/217/221/347/247/273/346/244/215/346/214/207/345/215/227.md +109 -33
  34. package/_packed_docs/sdk/INDEX.md +30 -18
  35. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +1 -1
  36. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +5 -5
  37. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +5 -5
  38. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +9 -9
  39. package/_packed_docs/sdk/README.md +11 -6
  40. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/345/256/236/346/226/275/350/256/241/345/210/222.md +400 -0
  41. package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/346/226/271/346/241/210.md +485 -0
  42. package/dist/auth.js +39 -45
  43. package/dist/auth.js.map +1 -1
  44. package/dist/client/control-messages.js +2 -2
  45. package/dist/client/control-messages.js.map +1 -1
  46. package/dist/client/delivery.d.ts +12 -1
  47. package/dist/client/delivery.js +281 -46
  48. package/dist/client/delivery.js.map +1 -1
  49. package/dist/client/lifecycle.js +13 -1
  50. package/dist/client/lifecycle.js.map +1 -1
  51. package/dist/client/rpc-budget.d.ts +21 -0
  52. package/dist/client/rpc-budget.js +82 -0
  53. package/dist/client/rpc-budget.js.map +1 -0
  54. package/dist/client/rpc-pipeline.d.ts +5 -0
  55. package/dist/client/rpc-pipeline.js +227 -63
  56. package/dist/client/rpc-pipeline.js.map +1 -1
  57. package/dist/client/sync-channels.d.ts +90 -0
  58. package/dist/client/sync-channels.js +508 -0
  59. package/dist/client/sync-channels.js.map +1 -0
  60. package/dist/client/v2-e2ee.d.ts +5 -1
  61. package/dist/client/v2-e2ee.js +420 -303
  62. package/dist/client/v2-e2ee.js.map +1 -1
  63. package/dist/client.d.ts +17 -0
  64. package/dist/client.js +373 -50
  65. package/dist/client.js.map +1 -1
  66. package/dist/events.d.ts +1 -0
  67. package/dist/events.js +5 -0
  68. package/dist/events.js.map +1 -1
  69. package/dist/index.d.ts +3 -1
  70. package/dist/index.js +1 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/mail-client.d.ts +101 -0
  73. package/dist/mail-client.js +369 -0
  74. package/dist/mail-client.js.map +1 -0
  75. package/dist/pubsub/binding.js +23 -3
  76. package/dist/pubsub/binding.js.map +1 -1
  77. package/dist/pubsub/channel.d.ts +15 -3
  78. package/dist/pubsub/channel.js +114 -7
  79. package/dist/pubsub/channel.js.map +1 -1
  80. package/dist/pubsub/client.d.ts +17 -2
  81. package/dist/pubsub/client.js +205 -40
  82. package/dist/pubsub/client.js.map +1 -1
  83. package/dist/pubsub/credentials.d.ts +3 -0
  84. package/dist/pubsub/credentials.js +57 -20
  85. package/dist/pubsub/credentials.js.map +1 -1
  86. package/dist/pubsub/index.d.ts +5 -2
  87. package/dist/pubsub/index.js +70 -54
  88. package/dist/pubsub/index.js.map +1 -1
  89. package/dist/pubsub/service.d.ts +9 -0
  90. package/dist/pubsub/service.js +266 -20
  91. package/dist/pubsub/service.js.map +1 -1
  92. package/dist/pubsub/types.d.ts +13 -1
  93. package/dist/storage/lowlevel.d.ts +3 -1
  94. package/dist/storage/lowlevel.js +13 -0
  95. package/dist/storage/lowlevel.js.map +1 -1
  96. package/dist/storage/types.d.ts +10 -0
  97. package/dist/storage/types.js.map +1 -1
  98. package/dist/storage/vfs.d.ts +3 -1
  99. package/dist/storage/vfs.js +2 -2
  100. package/dist/storage/vfs.js.map +1 -1
  101. package/dist/tools/cross-sdk-agent.d.ts +55 -5
  102. package/dist/tools/cross-sdk-agent.js +88 -21
  103. package/dist/tools/cross-sdk-agent.js.map +1 -1
  104. package/dist/transport.d.ts +16 -0
  105. package/dist/transport.js +129 -7
  106. package/dist/transport.js.map +1 -1
  107. package/dist/udp-standby.d.ts +89 -0
  108. package/dist/udp-standby.js +487 -0
  109. package/dist/udp-standby.js.map +1 -0
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +5 -1
  113. package/scripts/patch-imapflow-external.mjs +69 -0
@@ -1,5 +1,6 @@
1
1
  import { PubSubError } from './errors.js';
2
2
  import { FairScheduler } from './scheduler.js';
3
+ import { matchPattern, publisherFilter, validatePattern } from './channel.js';
3
4
  import { digest } from '../org-protocol.js';
4
5
  const copy = (v) => v === undefined ? v : JSON.parse(JSON.stringify(v));
5
6
  const json = (v) => { if (v === null || typeof v !== 'object')
@@ -16,6 +17,9 @@ export class EventService {
16
17
  clock;
17
18
  limits;
18
19
  state;
20
+ publisherResolver;
21
+ publisherProfiles = new Map();
22
+ publisherLookups = new Map();
19
23
  deliveryLeases = new Map();
20
24
  lifecycleGeneration = 0;
21
25
  generation = 0;
@@ -33,6 +37,8 @@ export class EventService {
33
37
  requests = new Map();
34
38
  published = new Map();
35
39
  counters = new Map();
40
+ publicationSummaries = new Map();
41
+ publicationBytes = 0;
36
42
  scheduler;
37
43
  snapshots = new Map();
38
44
  snapshotRefs = new Map();
@@ -82,42 +88,101 @@ export class EventService {
82
88
  return this; this.starting = this.initialize().finally(() => { this.starting = undefined; }); return this.starting; }
83
89
  async initialize() {
84
90
  const saved = await this.state?.load?.();
91
+ this.leases.clear();
92
+ this.leaseSizes.clear();
93
+ this.leaseBytes = this.subscriptionBytes = 0;
94
+ this.nextLeaseExpiry = Infinity;
95
+ this.subscriptions.clear();
96
+ this.subscriptionsByEvent.clear();
97
+ this.snapshots.clear();
85
98
  if (saved && saved.service_aid === this.service_aid) {
86
- this.catalog = copy(saved.catalog ?? this.catalog);
87
- this.allowlist = copy(saved.allowlist ?? this.allowlist);
99
+ this.catalog = copy({ ...saved.catalog, ...this.catalog });
100
+ this.allowlist = copy({ ...saved.allowlist, ...this.allowlist });
88
101
  this.issuedGrants = new Map(saved.issued_grants ?? []);
89
102
  this.pausedCids = new Set(saved.paused_cids ?? []);
90
103
  }
91
104
  this.validateCatalog(this.catalog);
92
105
  this.refreshRevision();
93
- await this.saveState();
94
106
  await this.credentials?.refreshRevocations?.();
107
+ if (saved && saved.service_aid === this.service_aid) {
108
+ for (const [id, value] of saved.leases ?? []) {
109
+ const lease = copy(value);
110
+ if (lease.expires_at <= this.clock() || lease.cid && (this.pausedCids.has(lease.cid) || this.credentials?.isRevoked?.(lease.grant ?? lease.cid)))
111
+ continue;
112
+ lease.scope = lease.scope.filter(item => !!this.catalog[item.event_name] && (lease.auth_mode !== 'allowlist' || this.allowlisted(lease.aid, item)));
113
+ if (!lease.scope.length)
114
+ continue;
115
+ if (lease.auth_mode === 'credential') {
116
+ if (!this.credentials || !lease.grant)
117
+ continue;
118
+ try {
119
+ const credentials = this.credentials;
120
+ const verified = credentials._restoreVerificationContext ? await credentials._restoreVerificationContext(lease.grant, lease.aid, lease.scope, lease.verification_context) : await credentials.verify(lease.grant, lease.aid, lease.scope, undefined);
121
+ lease.expires_at = Math.min(lease.expires_at, Number(verified.expires_at ?? lease.expires_at));
122
+ lease.authorization_until = Math.min(lease.expires_at, Number(verified.proof_expires_at ?? lease.expires_at));
123
+ if (lease.authorization_until <= this.clock())
124
+ continue;
125
+ if (verified.verification_context !== undefined)
126
+ lease.verification_context = verified.verification_context;
127
+ }
128
+ catch (error) {
129
+ if (error instanceof PubSubError && ['FORBIDDEN', 'REAUTH_REQUIRED'].includes(error.code))
130
+ continue;
131
+ throw error;
132
+ }
133
+ }
134
+ this.leases.set(id, lease);
135
+ const size = this.leaseSize(lease);
136
+ this.leaseSizes.set(id, size);
137
+ this.leaseBytes += size;
138
+ this.nextLeaseExpiry = Math.min(this.nextLeaseExpiry, lease.expires_at);
139
+ }
140
+ for (const [key, value] of saved.subscriptions ?? []) {
141
+ const sub = copy(value), lease = this.leases.get(sub.leaseId);
142
+ sub.filter = publisherFilter(sub.filter);
143
+ sub.size = new TextEncoder().encode(key + sub.aid + sub.leaseId + json(sub.filter)).byteLength + 128;
144
+ if (!lease || lease.aid !== sub.aid || !lease.scope.some(item => item.event_name === sub.event && item.action === 'subscribe'))
145
+ continue;
146
+ this.subscriptions.set(key, sub);
147
+ this.subscriptionBytes += sub.size;
148
+ this.generation = Math.max(this.generation, sub.generation);
149
+ let view = this.subscriptionsByEvent.get(sub.event);
150
+ if (!view)
151
+ this.subscriptionsByEvent.set(sub.event, view = new Map());
152
+ view.set(key, sub);
153
+ }
154
+ this.pruneSubscriptions();
155
+ }
156
+ await this.saveState();
95
157
  this.scheduler = new FairScheduler(task => this.deliver(task), { concurrency: Math.min(4, this.limits.event_concurrency ?? 4), quantum: this.limits.quantum ?? 4096, maxQueued: this.limits.delivery_items ?? 4096, maxBytes: Math.min(this.limits.delivery_bytes ?? 8 * 1024 * 1024, this.limit('body_bytes', 64 * 1024 * 1024), 64 * 1024 * 1024), perAidQueued: this.limits.per_aid_items ?? 512, perAidBytes: this.limits.per_aid_bytes ?? 2 * 1024 * 1024, costOverhead: this.limits.cost_overhead ?? 128, onDrop: (task, reason) => { this.deliveryLeases.delete(task.message_id); this.counter(task.event_name)[reason === 'failed' ? 'failed' : 'skipped_revoked']++; } });
96
158
  this.scheduler.onCapacity(() => this.capacityChanged());
97
159
  this.counters.clear();
160
+ this.publicationSummaries.clear();
161
+ this.publicationBytes = 0;
162
+ this.tokenKey = uuid();
98
163
  this.since = this.clock();
99
164
  this.closed = false;
100
165
  this.running = true;
101
166
  await this.scheduler.start();
102
167
  return this;
103
168
  }
104
- saveState() { const value = copy({ service_aid: this.service_aid, catalog: this.catalog, allowlist: this.allowlist, issued_grants: [...this.issuedGrants], paused_cids: [...this.pausedCids] }); const write = this.stateWrite.then(async () => { await this.state?.save?.(value); }); this.stateWrite = write.catch(() => { }); return write; }
169
+ saveState() { const value = copy({ service_aid: this.service_aid, catalog: this.catalog, allowlist: this.allowlist, issued_grants: [...this.issuedGrants], paused_cids: [...this.pausedCids], leases: [...this.leases], subscriptions: [...this.subscriptions] }); const write = this.stateWrite.then(async () => { await this.state?.save?.(value); }); this.stateWrite = write.catch(() => { }); return write; }
105
170
  async close() { if (this.starting) {
106
171
  try {
107
172
  await this.starting;
108
173
  }
109
174
  catch { }
110
175
  } this.lifecycleGeneration++; this.closed = true; this.running = false; this.capacityChanged(); await this.fanoutTask; if (this.scheduler)
111
- await this.scheduler.close(); this.scheduler = undefined; this.fanout.clear(); this.fanoutReady = []; this.fanoutCount = this.fanoutBytes = this.snapshotBytes = 0; this.snapshotRefs.clear(); this.deliveryLeases.clear(); this.leases.clear(); this.leaseSizes.clear(); this.leaseBytes = this.subscriptionBytes = 0; this.nextLeaseExpiry = Infinity; this.subscriptions.clear(); this.subscriptionsByEvent.clear(); this.snapshots.clear(); this.credentials?.clearVerified?.(); this.requests.clear(); this.published.clear(); await this.stateWrite; }
176
+ await this.scheduler.close(); this.scheduler = undefined; this.fanout.clear(); this.fanoutReady = []; this.fanoutCount = this.fanoutBytes = this.snapshotBytes = 0; this.snapshotRefs.clear(); this.deliveryLeases.clear(); this.leases.clear(); this.leaseSizes.clear(); this.leaseBytes = this.subscriptionBytes = 0; this.nextLeaseExpiry = Infinity; this.subscriptions.clear(); this.subscriptionsByEvent.clear(); this.snapshots.clear(); this.credentials?.clearVerified?.(); this.publisherProfiles.clear(); this.publisherLookups.clear(); this.requests.clear(); this.published.clear(); this.publicationSummaries.clear(); this.publicationBytes = 0; await this.stateWrite; }
112
177
  async drain() { while (this.fanoutTask)
113
178
  await this.fanoutTask; await this.scheduler?.drain(); }
114
179
  setSend(send) { this.send = send; }
115
180
  async updateConfig(config) { if (config.catalog) {
116
181
  this.validateCatalog(config.catalog);
117
- this.catalog = copy(config.catalog);
182
+ this.catalog = copy({ ...this.catalog, ...config.catalog });
118
183
  this.refreshRevision();
119
184
  } if (config.allowlist)
120
- this.allowlist = copy(config.allowlist); for (const [id, lease] of this.leases) {
185
+ this.allowlist = copy({ ...this.allowlist, ...config.allowlist }); for (const [id, lease] of this.leases) {
121
186
  lease.scope = lease.scope.filter(item => !!this.catalog[item.event_name] && (lease.auth_mode !== 'allowlist' || this.allowlisted(lease.aid, item)));
122
187
  if (!lease.scope.length)
123
188
  this.removeLease(id);
@@ -166,6 +231,8 @@ export class EventService {
166
231
  return this.catalogOp(senderAid, request, undefined, undefined, local);
167
232
  if (op === 'stats')
168
233
  return this.statsOp(senderAid, request);
234
+ if (op === 'publications' || op === 'subscriptions')
235
+ return this.listingOp(senderAid, request, local);
169
236
  const id = request.request_id;
170
237
  if (!id)
171
238
  throw new PubSubError('INVALID_REQUEST');
@@ -183,7 +250,8 @@ export class EventService {
183
250
  throw new PubSubError('OVERLOADED');
184
251
  const entry = { fingerprint: fp, expiresAt: request.expires_at ?? this.clock() + 60_000 };
185
252
  this.requests.set(key, entry);
186
- const promise = Promise.resolve().then(() => this.dispatch(senderAid, request, route));
253
+ const promise = Promise.resolve().then(async () => { const result = await this.dispatch(senderAid, request, route); if (this.state?.save && ['authorize', 'renew', 'subscribe', 'unsubscribe'].includes(op))
254
+ await this.saveState(); return result; });
187
255
  entry.promise = promise;
188
256
  try {
189
257
  const result = await promise;
@@ -248,6 +316,7 @@ export class EventService {
248
316
  let expires = this.clock() + 12_000_000;
249
317
  let cid;
250
318
  let proofExpires = expires;
319
+ let verificationContext;
251
320
  if (mode === 'allowlist') {
252
321
  if (scope.some(item => !this.allowlisted(sender, item)))
253
322
  throw new PubSubError('FORBIDDEN');
@@ -262,13 +331,14 @@ export class EventService {
262
331
  if (this.pausedCids.has(cid) || this.credentials.isRevoked?.(grant))
263
332
  throw new PubSubError('REAUTH_REQUIRED');
264
333
  proofExpires = Number(verified.proof_expires_at ?? expires);
334
+ verificationContext = verified.verification_context;
265
335
  }
266
336
  else
267
337
  throw new PubSubError('INVALID_AUTH_MODE');
268
338
  if (generation !== this.lifecycleGeneration || this.closed)
269
339
  throw new PubSubError('SERVICE_CLOSED');
270
340
  const grant = mode === 'credential' ? (request.grant ?? request.credential) : undefined;
271
- const lease = { lease_id: uuid(), aid: sender, ...(route ? { route: copy(route) } : {}), scope, auth_mode: mode, expires_at: expires, authorization_until: Math.min(expires, proofExpires), ...(cid ? { cid } : {}), ...(grant ? { grant } : {}) };
341
+ const lease = { lease_id: uuid(), aid: sender, ...(route ? { route: copy(route) } : {}), scope, auth_mode: mode, expires_at: expires, authorization_until: Math.min(expires, proofExpires), ...(cid ? { cid } : {}), ...(grant ? { grant } : {}), ...(verificationContext !== undefined ? { verification_context: verificationContext } : {}) };
272
342
  const size = this.leaseSize(lease);
273
343
  if (this.leases.size >= this.limit('lease_items', 16384) || this.leaseBytes + size > this.limit('lease_bytes', 8 * 1024 * 1024))
274
344
  throw new PubSubError('OVERLOADED');
@@ -305,7 +375,7 @@ export class EventService {
305
375
  throw new PubSubError('REAUTH_REQUIRED');
306
376
  }
307
377
  catch (error) {
308
- if (error instanceof PubSubError && error.code === 'TIMEOUT')
378
+ if (!(error instanceof PubSubError) || !['FORBIDDEN', 'REAUTH_REQUIRED', 'CREDENTIAL_REVOKED', 'CREDENTIAL_EXPIRED'].includes(error.code))
309
379
  throw error;
310
380
  this.removeLease(lease.lease_id);
311
381
  this.pruneSubscriptions();
@@ -313,7 +383,7 @@ export class EventService {
313
383
  }
314
384
  }
315
385
  async renew(sender, request) { const old = this.lease(sender, request.lease_id); if (old.auth_mode === 'credential')
316
- await this.credentials?.assertNotRevoked?.(old.grant ?? old.cid, request.expires_at); const now = this.clock(); let expires = now + 12_000_000, proofExpires = expires; let grant = old.grant, cid = old.cid; if (old.auth_mode === 'credential') {
386
+ await this.credentials?.assertNotRevoked?.(old.grant ?? old.cid, request.expires_at); const now = this.clock(); let expires = now + 12_000_000, proofExpires = expires; let grant = old.grant, cid = old.cid; let verificationContext = old.verification_context; if (old.auth_mode === 'credential') {
317
387
  grant = request.grant ?? old.grant;
318
388
  if (!this.credentials || !grant)
319
389
  throw new PubSubError('REAUTH_REQUIRED');
@@ -321,18 +391,41 @@ export class EventService {
321
391
  const verified = await this.credentials.verify(grant, sender, old.scope, request, request.expires_at);
322
392
  expires = Math.min(expires, Number(verified.expires_at));
323
393
  proofExpires = Number(verified.proof_expires_at ?? expires);
394
+ verificationContext = verified.verification_context;
324
395
  cid = String(verified.cid ?? grant.cid ?? '');
325
396
  if (Math.min(expires, proofExpires) <= now || this.pausedCids.has(cid) || this.credentials.isRevoked?.(grant))
326
397
  throw new PubSubError('REAUTH_REQUIRED');
327
398
  } if (this.lease(sender, request.lease_id) !== old)
328
- throw new PubSubError('REAUTH_REQUIRED'); const updated = { ...old, grant, cid, expires_at: expires, authorization_until: Math.min(expires, proofExpires) }; const change = this.leaseSize(updated) - (this.leaseSizes.get(old.lease_id) ?? 0); if (this.leaseBytes + change > this.limit('lease_bytes', 8 * 1024 * 1024))
399
+ throw new PubSubError('REAUTH_REQUIRED'); const updated = { ...old, grant, cid, expires_at: expires, authorization_until: Math.min(expires, proofExpires), ...(verificationContext !== undefined ? { verification_context: verificationContext } : {}) }; const change = this.leaseSize(updated) - (this.leaseSizes.get(old.lease_id) ?? 0); if (this.leaseBytes + change > this.limit('lease_bytes', 8 * 1024 * 1024))
329
400
  throw new PubSubError('OVERLOADED'); Object.assign(old, updated); this.leaseBytes += change; this.leaseSizes.set(old.lease_id, this.leaseSize(updated)); this.nextLeaseExpiry = Math.min(this.nextLeaseExpiry, expires); return this.leaseResult(old); }
330
- events(request) { const names = request.events ?? (request.event_name ? [request.event_name] : []); if (!names.length || names.some(name => typeof name !== 'string' || !this.catalog[name]))
331
- throw new PubSubError('UNKNOWN_EVENT'); return [...new Set(names)]; }
401
+ events(request, lease, unsubscribe = false) {
402
+ const names = request.events ?? (request.event_name ? [request.event_name] : []);
403
+ if (!Array.isArray(names) || !names.length)
404
+ throw new PubSubError('UNKNOWN_EVENT');
405
+ const events = new Set();
406
+ for (const name of names) {
407
+ validatePattern(name);
408
+ if (!name.includes('*')) {
409
+ if (!this.catalog[name])
410
+ throw new PubSubError('UNKNOWN_EVENT');
411
+ this.lease(lease.aid, lease.lease_id, [{ event_name: name, action: 'subscribe' }]);
412
+ events.add(name);
413
+ continue;
414
+ }
415
+ const matches = unsubscribe ? [...this.subscriptions.values()].filter(sub => sub.leaseId === lease.lease_id && matchPattern(name, sub.event)).map(sub => sub.event) : Object.keys(this.catalog).filter(event => matchPattern(name, event));
416
+ if (!unsubscribe && !matches.length)
417
+ throw new PubSubError('UNKNOWN_EVENT');
418
+ const authorized = matches.filter(event => lease.scope.some(item => item.event_name === event && item.action === 'subscribe'));
419
+ if (!unsubscribe && !authorized.length)
420
+ throw new PubSubError('FORBIDDEN');
421
+ authorized.forEach(event => events.add(event));
422
+ }
423
+ return [...events];
424
+ }
332
425
  subscriptionKey(aid, event, route) { return route ? json([aid, route.device_id, route.slot_id, event]) : `${aid}:${event}`; }
333
- async subscribe(sender, request) { const events = this.events(request); const lease = this.lease(sender, request.lease_id, events.map(event_name => ({ event_name, action: 'subscribe' }))); const additions = events.map(name => { const key = this.subscriptionKey(sender, name, lease.route); return { name, key, size: new TextEncoder().encode(key + sender + lease.lease_id).byteLength + 128 }; }); const added = additions.filter(item => !this.subscriptions.has(item.key)); const bytes = additions.reduce((sum, item) => sum + item.size - (this.subscriptions.get(item.key)?.size ?? 0), 0); if (this.subscriptions.size + added.length > this.limit('subscription_items', 16384) || this.subscriptionBytes + bytes > this.limit('subscription_bytes', 8 * 1024 * 1024))
426
+ async subscribe(sender, request) { const filter = publisherFilter(request), lease = this.lease(sender, request.lease_id), events = this.events(request, lease); const additions = events.map(name => { const key = this.subscriptionKey(sender, name, lease.route); return { name, key, size: new TextEncoder().encode(key + sender + lease.lease_id + json(filter)).byteLength + 128 }; }); const added = additions.filter(item => !this.subscriptions.has(item.key)); const bytes = additions.reduce((sum, item) => sum + item.size - (this.subscriptions.get(item.key)?.size ?? 0), 0); if (this.subscriptions.size + added.length > this.limit('subscription_items', 16384) || this.subscriptionBytes + bytes > this.limit('subscription_bytes', 8 * 1024 * 1024))
334
427
  throw new PubSubError('OVERLOADED'); for (const item of additions) {
335
- const sub = { leaseId: lease.lease_id, generation: ++this.generation, aid: sender, event: item.name, size: item.size };
428
+ const sub = { leaseId: lease.lease_id, generation: ++this.generation, aid: sender, event: item.name, size: item.size, filter };
336
429
  this.subscriptionBytes += item.size - (this.subscriptions.get(item.key)?.size ?? 0);
337
430
  this.subscriptions.set(item.key, sub);
338
431
  let view = this.subscriptionsByEvent.get(item.name);
@@ -343,14 +436,14 @@ export class EventService {
343
436
  } return { status: 'subscribed', events }; }
344
437
  removeSubscription(key, sub) { this.subscriptions.delete(key); this.subscriptionBytes -= sub.size; const view = this.subscriptionsByEvent.get(sub.event); view?.delete(key); if (!view?.size)
345
438
  this.subscriptionsByEvent.delete(sub.event); this.snapshots.delete(sub.event); }
346
- async unsubscribe(sender, request) { const events = this.events(request); const lease = this.lease(sender, request.lease_id, events.map(event_name => ({ event_name, action: 'subscribe' }))); for (const name of events) {
439
+ async unsubscribe(sender, request) { const lease = this.lease(sender, request.lease_id), events = this.events(request, lease, true); for (const name of events) {
347
440
  const key = this.subscriptionKey(sender, name, lease.route), sub = this.subscriptions.get(key);
348
441
  if (sub?.leaseId === lease.lease_id)
349
442
  this.removeSubscription(key, sub);
350
443
  } return { status: 'unsubscribed', events }; }
351
444
  snapshot(event) { let snapshot = this.snapshots.get(event); if (!snapshot) {
352
- const entries = [...(this.subscriptionsByEvent.get(event)?.entries() ?? [])].map(([key, sub]) => [sub.aid, { leaseId: sub.leaseId, generation: sub.generation, key }]);
353
- snapshot = { entries, size: 64 + entries.reduce((sum, [target, sub]) => sum + new TextEncoder().encode(target + sub.leaseId + sub.key).byteLength + 128, 0) };
445
+ const entries = [...(this.subscriptionsByEvent.get(event)?.entries() ?? [])].map(([key, sub]) => [sub.aid, { leaseId: sub.leaseId, generation: sub.generation, key, filter: sub.filter }]);
446
+ snapshot = { entries, size: 64 + entries.reduce((sum, [target, sub]) => sum + new TextEncoder().encode(target + sub.leaseId + sub.key + json(sub.filter ?? {})).byteLength + 128, 0) };
354
447
  this.snapshots.set(event, snapshot);
355
448
  } return snapshot; }
356
449
  pruneSubscriptions() { for (const [key, sub] of this.subscriptions) {
@@ -443,6 +536,140 @@ export class EventService {
443
536
  } return true; }
444
537
  counter(name) { const c = this.counters.get(name) ?? {}; for (const k of ['published', 'matched', 'submitted', 'failed', 'unconfirmed', 'deduplicated', 'rejected', 'skipped_expired', 'skipped_overloaded', 'skipped_revoked'])
445
538
  c[k] ??= 0; this.counters.set(name, c); return c; }
539
+ async listingOp(sender, request, local) {
540
+ const generation = this.lifecycleGeneration, check = () => { if (generation !== this.lifecycleGeneration || this.closed || !this.running)
541
+ throw new PubSubError('SERVICE_CLOSED'); };
542
+ const action = request.op === 'publications' ? 'publish' : 'subscribe', lease = local ? undefined : this.lease(sender, request.lease_id);
543
+ let scope = local ? Object.keys(this.catalog).sort() : [...new Set(lease.scope.filter(item => item.action === action).map(item => item.event_name))].sort();
544
+ if (!local && !scope.length)
545
+ throw new PubSubError('FORBIDDEN');
546
+ const limit = request.limit ?? 50;
547
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200)
548
+ throw new PubSubError('INVALID_LIMIT');
549
+ let binding = digest([request.op, local, sender, lease?.lease_id ?? null, scope, request.event_name ?? null, this.since]), pageExpires = Infinity;
550
+ let last;
551
+ if (request.page_token !== undefined) {
552
+ try {
553
+ const envelope = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(request.page_token), value => value.charCodeAt(0))));
554
+ if (envelope.signature !== digest([this.tokenKey, envelope.token]) || envelope.token.binding !== binding || envelope.token.expires_at <= this.clock() || !Array.isArray(envelope.token.last) || envelope.token.last.length !== (action === 'publish' ? 2 : 4) || envelope.token.last.some((value) => typeof value !== 'string'))
555
+ throw new Error();
556
+ last = envelope.token.last;
557
+ pageExpires = envelope.token.expires_at;
558
+ }
559
+ catch {
560
+ throw new PubSubError('INVALID_PAGE_TOKEN');
561
+ }
562
+ }
563
+ if (action === 'subscribe') {
564
+ const checked = new Set();
565
+ for (const sub of this.subscriptions.values()) {
566
+ if ((!local && sub.leaseId !== lease.lease_id) || !scope.includes(sub.event) || request.event_name && sub.event !== request.event_name || checked.has(sub.leaseId))
567
+ continue;
568
+ checked.add(sub.leaseId);
569
+ const current = this.leases.get(sub.leaseId);
570
+ if (!current)
571
+ continue;
572
+ try {
573
+ await this.refreshProof(this.lease(current.aid, current.lease_id), request.expires_at);
574
+ }
575
+ catch (error) {
576
+ check();
577
+ if (!(error instanceof PubSubError) || !['REAUTH_REQUIRED', 'FORBIDDEN'].includes(error.code))
578
+ throw error;
579
+ }
580
+ check();
581
+ }
582
+ this.pruneSubscriptions();
583
+ }
584
+ check();
585
+ const currentLease = local ? undefined : this.lease(sender, request.lease_id);
586
+ scope = local ? Object.keys(this.catalog).sort() : [...new Set(currentLease.scope.filter(item => item.action === action).map(item => item.event_name))].sort();
587
+ if (!local && !scope.length)
588
+ throw new PubSubError('FORBIDDEN');
589
+ const currentBinding = digest([request.op, local, sender, currentLease?.lease_id ?? null, scope, request.event_name ?? null, this.since]);
590
+ if (request.page_token !== undefined && (binding !== currentBinding || pageExpires <= this.clock()))
591
+ throw new PubSubError('INVALID_PAGE_TOKEN');
592
+ binding = currentBinding;
593
+ const key = (item) => action === 'publish' ? [String(item.event_name), String(item.publisher_aid)] : [String(item.event_name), String(item.subscriber_aid), String(item.device_id), String(item.slot_id)];
594
+ const compare = (a, b) => { for (let i = 0; i < a.length; i++) {
595
+ const left = Array.from(a[i], value => value.codePointAt(0)), right = Array.from(b[i], value => value.codePointAt(0));
596
+ for (let j = 0; j < Math.min(left.length, right.length); j++)
597
+ if (left[j] !== right[j])
598
+ return left[j] - right[j];
599
+ if (left.length !== right.length)
600
+ return left.length - right.length;
601
+ } return 0; };
602
+ const rows = action === 'publish' ? [...this.publicationSummaries.values()].filter(item => scope.includes(item.event_name) && (!request.event_name || item.event_name === request.event_name) && (local || item.publisher_aid === sender)).map(item => copy(item)) : [...this.subscriptions.values()].filter(sub => { const current = this.leases.get(sub.leaseId); return current && current.expires_at > this.clock() && (current.auth_mode !== 'credential' || current.authorization_until > this.clock()) && current.scope.some(item => item.event_name === sub.event && item.action === 'subscribe') && (local || sub.leaseId === currentLease.lease_id) && scope.includes(sub.event) && (!request.event_name || sub.event === request.event_name); }).map(sub => { const current = this.leases.get(sub.leaseId); return { subscriber_aid: sub.aid, event_name: sub.event, device_id: current.route?.device_id ?? '', slot_id: current.route?.slot_id ?? '', expires_at: current.expires_at }; });
603
+ const remaining = rows.sort((a, b) => compare(key(a), key(b))).filter(item => !last || compare(key(item), last) > 0), asOf = this.clock();
604
+ const result = (items) => {
605
+ const token = { binding, last: items.length ? key(items[items.length - 1]) : last, expires_at: Math.min(this.clock() + 60000, lease?.expires_at ?? Infinity) };
606
+ const next = items.length && items.length < remaining.length ? btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify({ token, signature: digest([this.tokenKey, token]) })))) : null;
607
+ return { service_aid: this.service_aid, since: this.since, as_of: asOf, items, next_page_token: next };
608
+ };
609
+ const fits = (items) => new TextEncoder().encode(json({ type: 'app.pubsub.v1', op: `${request.op}.result`, service_aid: this.service_aid, request_id: request.request_id, expires_at: request.expires_at, ...(request.event_id ? { event_id: request.event_id } : {}), result: result(items) })).byteLength <= Math.min(this.limit('catalog_page_bytes', 65536), 65536);
610
+ const items = [];
611
+ for (const item of remaining.slice(0, limit)) {
612
+ if (!fits([...items, item])) {
613
+ if (!items.length)
614
+ throw new PubSubError('MESSAGE_TOO_LARGE');
615
+ break;
616
+ }
617
+ items.push(item);
618
+ }
619
+ if (!fits(items))
620
+ throw new PubSubError('MESSAGE_TOO_LARGE');
621
+ return result(items);
622
+ }
623
+ waitPublisherProfile(lookup, deadline) {
624
+ return new Promise(resolve => { const timer = setTimeout(() => resolve(undefined), Math.max(0, deadline - this.clock())); lookup.then(profile => { clearTimeout(timer); resolve(profile); }, () => { clearTimeout(timer); resolve(undefined); }); });
625
+ }
626
+ publisherProfile(aid, deadline) {
627
+ const now = this.clock();
628
+ for (const [key, profile] of this.publisherProfiles)
629
+ if (profile.expiresAt <= now)
630
+ this.publisherProfiles.delete(key);
631
+ const cached = this.publisherProfiles.get(aid);
632
+ if (cached)
633
+ return Promise.resolve(cached);
634
+ if (deadline <= now)
635
+ return Promise.resolve(undefined);
636
+ const pending = this.publisherLookups.get(aid);
637
+ if (pending)
638
+ return this.waitPublisherProfile(pending, deadline);
639
+ if (!this.publisherResolver)
640
+ return Promise.resolve(undefined);
641
+ const generation = this.lifecycleGeneration;
642
+ const lookup = new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('publisher profile timeout')), 10000); Promise.resolve().then(() => this.publisherResolver(aid, 10000)).then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); }); }).then(value => {
643
+ const document = value;
644
+ if (generation !== this.lifecycleGeneration)
645
+ return undefined;
646
+ if (document?.verification?.status !== 'verified' || !document.frontmatter)
647
+ throw new Error('unverified publisher');
648
+ const profile = { ...(typeof document.frontmatter.type === 'string' ? { type: document.frontmatter.type } : {}), ...(typeof document.frontmatter.exttype === 'string' ? { exttype: document.frontmatter.exttype } : {}) };
649
+ if (this.publisherProfiles.size >= 1024)
650
+ this.publisherProfiles.delete(this.publisherProfiles.keys().next().value);
651
+ this.publisherProfiles.set(aid, { ...profile, expiresAt: this.clock() + 60000 });
652
+ return profile;
653
+ }).catch(() => { if (generation === this.lifecycleGeneration) {
654
+ if (this.publisherProfiles.size >= 1024)
655
+ this.publisherProfiles.delete(this.publisherProfiles.keys().next().value);
656
+ this.publisherProfiles.set(aid, { expiresAt: this.clock() + 5000 });
657
+ } return undefined; }).finally(() => { if (this.publisherLookups.get(aid) === lookup)
658
+ this.publisherLookups.delete(aid); });
659
+ this.publisherLookups.set(aid, lookup);
660
+ return this.waitPublisherProfile(lookup, deadline);
661
+ }
662
+ async publisherSnapshot(event, aid, deadline) {
663
+ const source = this.snapshot(event);
664
+ if (!source.entries.some(([, sub]) => Object.keys(sub.filter ?? {}).length))
665
+ return source;
666
+ const needsProfile = source.entries.some(([, sub]) => (sub.filter?.publisher_types || sub.filter?.publisher_exttypes) && (!sub.filter.publisher_aids || sub.filter.publisher_aids.some(pattern => matchPattern(pattern, aid)))), profile = needsProfile ? await this.publisherProfile(aid, deadline - 2000) : undefined;
667
+ const entries = this.snapshot(event).entries.filter(([, sub]) => {
668
+ const filter = sub.filter;
669
+ return (!filter?.publisher_aids || filter.publisher_aids.some(pattern => matchPattern(pattern, aid))) && (!filter?.publisher_types || typeof profile?.type === 'string' && filter.publisher_types.some(pattern => matchPattern(pattern, profile.type))) && (!filter?.publisher_exttypes || typeof profile?.exttype === 'string' && filter.publisher_exttypes.some(pattern => matchPattern(pattern, profile.exttype)));
670
+ });
671
+ return { entries, size: 64 + entries.reduce((sum, [target, sub]) => sum + new TextEncoder().encode(target + sub.leaseId + sub.key + json(sub.filter ?? {})).byteLength + 128, 0) };
672
+ }
446
673
  async publish(sender, request, reservation) {
447
674
  const event = request.event_name;
448
675
  if (!event || !this.catalog[event])
@@ -467,7 +694,24 @@ export class EventService {
467
694
  this.counter(event).deduplicated++;
468
695
  return copy(prior.result);
469
696
  }
470
- const size = new TextEncoder().encode(json(payload)).byteLength + 128, queue = this.fanout.get(sender) ?? [], snapshotView = this.snapshot(event), snapshot = snapshotView.entries, snapshotSize = snapshotView.size;
697
+ const lifecycle = this.lifecycleGeneration, snapshotView = await this.publisherSnapshot(event, sender, deadline);
698
+ if (lifecycle !== this.lifecycleGeneration || !this.running)
699
+ throw new PubSubError('SERVICE_CLOSED');
700
+ this.lease(sender, request.lease_id, [{ event_name: event, action: 'publish' }]);
701
+ if (deadline - this.clock() < 1000)
702
+ throw new PubSubError('EXPIRED');
703
+ const duplicate = this.published.get(key);
704
+ if (duplicate) {
705
+ if (duplicate.fingerprint !== fingerprint)
706
+ throw new PubSubError('EVENT_CONFLICT');
707
+ this.counter(event).deduplicated++;
708
+ return copy(duplicate.result);
709
+ }
710
+ const summaryKey = json([sender, event]), previous = this.publicationSummaries.get(summaryKey), summary = { publisher_aid: sender, event_name: event, published: (previous?.published ?? 0) + 1, first_published_at: previous?.first_published_at ?? now, last_published_at: now };
711
+ const summaryBytes = new TextEncoder().encode(json(summary)).byteLength, previousBytes = previous ? new TextEncoder().encode(json(previous)).byteLength : 0;
712
+ if (!previous && this.publicationSummaries.size >= this.limit('publication_items', 8192) || this.publicationBytes + summaryBytes - previousBytes > this.limit('publication_bytes', 4 * 1024 * 1024))
713
+ throw new PubSubError('OVERLOADED');
714
+ const size = new TextEncoder().encode(json(payload)).byteLength + 128, queue = this.fanout.get(sender) ?? [], snapshot = snapshotView.entries, snapshotSize = snapshotView.size;
471
715
  if (size - 128 > this.limit('event_bytes', 65536) || !this.publishCapacityAvailable(size, reservation?.bytes ?? 0) || queue.length >= this.limit('accepted_per_aid', 128) || this.published.size >= this.limit('dedup_items', 8192) || (!this.snapshotRefs.has(snapshot) && (this.snapshotRefs.size >= this.limit('snapshot_items', 256) || this.snapshotBytes + snapshotSize > this.limit('snapshot_bytes', 8 * 1024 * 1024))))
472
716
  throw new PubSubError('OVERLOADED');
473
717
  const pin = this.snapshotRefs.get(snapshot);
@@ -492,6 +736,8 @@ export class EventService {
492
736
  this.published.set(key, { fingerprint, result, expiresAt: deadline });
493
737
  this.counter(event).published++;
494
738
  this.capacityChanged();
739
+ this.publicationSummaries.set(summaryKey, summary);
740
+ this.publicationBytes += summaryBytes - previousBytes;
495
741
  if (!this.fanoutTask)
496
742
  this.fanoutTask = new Promise(resolve => setTimeout(resolve, 0)).then(() => this.runFanout()).finally(() => { this.fanoutTask = undefined; });
497
743
  return result;