@volter/teams 0.2.55 → 0.2.56
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/package.json +1 -1
- package/server/published-events.mjs +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,6 +12,16 @@ const stmt = (sql, ...params) => ({sql, params});
|
|
|
12
12
|
const batches = (list, size = 90) => Array.from({length: Math.ceil(list.length / size)}, (_, at) => list.slice(at * size, (at + 1) * size));
|
|
13
13
|
const holes = list => list.map(() => '?').join(',');
|
|
14
14
|
const EVENT_REPLAY_BATCH = 32;
|
|
15
|
+
// A publication is written about four times: the event, its resource's snapshot, its idempotent answer, and a
|
|
16
|
+
// webhook delivery. Past the host's maximum a write does not fail, it resets the server under every machine's link:
|
|
17
|
+
// an event that would not fit under the limit beside a mebibyte kept for retention is refused, and writes nothing.
|
|
18
|
+
const EVENT_COPIES = 4, EVENT_RESERVE = 1024 * 1024;
|
|
19
|
+
async function eventHeadroomSpent(api, res, bytes) {
|
|
20
|
+
const usage = await api.storageUsage?.();
|
|
21
|
+
if (!usage || usage.used + bytes * EVENT_COPIES + EVENT_RESERVE < usage.limit) return false;
|
|
22
|
+
res._auditRecord = null; res._idempotencyRecord = null;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
15
25
|
const fail = (status,code,message) => {throw Object.assign(new Error(message),{status,code});};
|
|
16
26
|
const text = (value,label) => {if(typeof value!=='string'||!value||value.length>256||/[\x00-\x1f]/.test(value))fail(400,'invalid_request',`${label} must be a nonempty string of at most 256 characters`);return value;};
|
|
17
27
|
const eventOf = row => ({id:row.id,org:row.team_id,at:row.created_at,actor:JSON.parse(row.actor_json),kind:row.kind,resource:{kind:row.resource_kind,id:row.resource_id},data:JSON.parse(row.data_json)});
|
|
@@ -93,8 +103,10 @@ export async function publishedEventRoute(ctx) {
|
|
|
93
103
|
if(body.actor!=null){exact(body.actor,['session']);if(body.actor.session!=null){text(body.actor.session,'actor.session');if(!body.actor.session.startsWith('session:'))fail(400,'invalid_request','actor.session must name a session');actor.session=body.actor.session;}}
|
|
94
104
|
const source=await api.store.one(OWN_SOURCE,[auth.team_id,publication[1],auth.principal_id,sourceMachine(auth)]);
|
|
95
105
|
if(!source)fail(404,'not_found','event source not found');
|
|
106
|
+
const spent=await eventHeadroomSpent(api,res,Buffer.byteLength(JSON.stringify(body.data)));
|
|
96
107
|
const old=await api.store.one('SELECT * FROM org_events WHERE id=?',[body.id]);
|
|
97
108
|
if(old){if(old.team_id!==auth.team_id||old.source_id!==publication[1]||old.kind!==body.kind||old.resource_id!==body.resource.id||old.source_revision!==body.revision||canonicalJson(JSON.parse(old.data_json))!==canonicalJson(body.data)||canonicalJson(JSON.parse(old.actor_json))!==canonicalJson(actor))fail(409,'idempotency_conflict','event id already names another event');send(res,200,result(api,auth,eventOf(old)),requestId);return true;}
|
|
109
|
+
if(spent){res.setHeader('Retry-After','60');throw Object.assign(new Error('event deferred while Teams storage is full'),{status:507,code:'storage_full',retryable:true});}
|
|
98
110
|
const now=api.clock.nowMs(),guards=[...authorityGuards(auth,now),{sql:OWN_SOURCE,params:[auth.team_id,publication[1],auth.principal_id,sourceMachine(auth)],status:403,code:'forbidden',message:'event source authority changed'},
|
|
99
111
|
{sql:'SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM event_resources WHERE team_id=? AND source_id=? AND resource_kind=? AND resource_id=? AND source_revision>=?)',params:[auth.team_id,publication[1],body.resource.kind,body.resource.id,body.revision],status:409,code:'revision_mismatch',message:'resource publication is older than its current snapshot'}];
|
|
100
112
|
const event={id:body.id,org:auth.team_id,at:now,actor,kind:body.kind,resource:body.resource,data:body.data},response=result(api,auth,event);
|