@kubun/engine 0.10.2 → 0.10.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/lib/engine.d.ts +8 -0
- package/lib/engine.js +1 -1
- package/package.json +9 -9
package/lib/engine.d.ts
CHANGED
|
@@ -59,6 +59,14 @@ export type EngineParams = {
|
|
|
59
59
|
* current open-read behavior.
|
|
60
60
|
*/
|
|
61
61
|
defaultAccessLevel?: DefaultAccessLevel;
|
|
62
|
+
/**
|
|
63
|
+
* Maximum allowed future drift (in milliseconds) between an incoming
|
|
64
|
+
* mutation's HLC wall-time and the local clock. A mutation whose HLC
|
|
65
|
+
* wall-time exceeds `now + maxDriftMS` is rejected to guard against
|
|
66
|
+
* last-writer-wins poisoning from future-dated writes. The past direction is
|
|
67
|
+
* unbounded so offline-first writes always apply. Defaults to one hour.
|
|
68
|
+
*/
|
|
69
|
+
maxDriftMS?: number;
|
|
62
70
|
};
|
|
63
71
|
/**
|
|
64
72
|
* Optional pre-persist gate. Receives the synthesized post-apply
|
package/lib/engine.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createRevocationChecker as e}from"@enkaku/capability";import{createRuntime as t}from"@enkaku/runtime";import{asType as i,createValidator as s}from"@enkaku/schema";import{isSigningIdentity as r,stringifyToken as a,verifyToken as n}from"@enkaku/token";import{KubunDB as o}from"@kubun/db";import{createReadContext as l,createSchema as u}from"@kubun/graphql";import{HLC as c}from"@kubun/hlc";import{DocumentID as d}from"@kubun/id";import{getKubunLogger as h}from"@kubun/logger";import{applyMutation as p,convertPatchInput as m,createMutationOperations as g,WriteAccessDeniedError as f}from"@kubun/mutation";import{clusterToRecord as v,documentMutation as w,GraphModel as y}from"@kubun/protocol";import{GRAPH_STORE as D,getGraphStore as b,graphStoreDefinition as x}from"@kubun/store-graph";import{createP2PRevocationBackend as I,getP2PStore as M}from"@kubun/store-p2p";import{execute as A,GraphQLError as C,Kind as G,parse as k,subscribe as E,validate as R}from"graphql";import{createAccessChecker as O}from"./access-control.js";import{EngineEventBus as S}from"./events.js";import{checkMembership as L}from"./membership-check.js";import{computeMutationHash as F}from"./mutation-hash.js";import{runPolicies as T}from"./policies.js";import{createRegistry as j}from"./registry.js";let U=s(w);function q(e){if(null==e)return;let t=c.parse(e).wallTime;return Number.isFinite(t)?Math.floor(t/1e3):void 0}export function buildWriteAccessChecker(e){let{store:t,defaultAccessLevel:i,extraTokensByIssuer:s,revocationChecker:r}=e,a=new Map,n={getUserModelAccessDefault:(e,i,s)=>t.getUserModelAccessDefault(e,i,s),isMemberOfAnyGroup:async()=>!1,isMemberOfAnyCircle:async()=>!1,getModelInterfaces:async e=>{let i=a.get(e);if(null!=i)return i;let s=await t.getModelInterfaces(e);return a.set(e,s),s},revocationChecker:r};return(e,t)=>{var r;let a=(null==(r=t.cap)?void 0:Array.isArray(r)?r:[r])??[],o=s?.get(t.iss)??[],l=0===a.length&&0===o.length?void 0:[...a,...o];return O({viewerDID:t.iss,delegationTokens:l,db:n,defaultAccessLevel:i,atTime:q(t.hlc)})(e,"write")}}function P(){throw Error("Graph mutations require mutateGraph() — the core context does not sign or log writes. Use KubunEngine.mutateGraph() instead.")}function B(){throw Error("Access control mutations are not supported by the engine core — plugin override required.")}function _(){throw Error("Transaction mutations are not supported by the engine core — plugin override required (see plugin-rpc).")}let V={executeCreateMutation:P,executeSetMutation:P,executeUpdateMutation:P,executeRemoveMutation:P,executeSetModelAccessDefaults:B,executeRemoveModelAccessDefaults:B,executeSetDocumentAccessOverride:B,executeRemoveDocumentAccessOverride:B,beginTransaction:_,commitTransaction:_,rollbackTransaction:_};export class KubunEngine{#e=[];#t=[];#i;#s;#r;#a={};#n;#o;#l;#u={};#c=new Map;#d=[];#h;#p;#m={};#g={};constructor(e){this.#i=e.db instanceof o?e.db:new o({adapter:e.db}),this.#i.register(x),this.#o=e.identity,this.#s=e.defaultAccessLevel??{read:"anyone",write:"only_owner"},this.#n=new c({nodeID:e.identity.id}),this.#l=e.logger??h("engine"),this.#r=e.eventBus??new S({logger:this.#l.getChild("events")}),this.#p=t(e.runtime),this.#h=j();let i=e.plugins??[],s={engine:this,graph:{execute:e=>this.#f(e),subscribe:e=>this.#v(e),applyVerifiedMutation:e=>this.#w(e),applyVerifiedMutations:e=>this.#y(e)},db:this.#i,runtime:this.#p,identity:this.#o,eventBus:this.#r,hlc:this.#n,getLogger:e=>this.#l.getChild(e)};for(let e of i){let t=e(s);this.#d.push(t),null!=t.api&&this.#h.registerPlugin(t.name,t.api)}this.#h.closeGate();let r=new Set;for(let e of this.#d){if(null!=e.schemaExtension&&this.#c.set(e.name,e.schemaExtension),null!=e.createContextFactory){if(r.has(e.name))throw Error(`Duplicate plugin namespace: ${e.name}`);r.add(e.name),this.#e.push({name:e.name,factory:e.createContextFactory()})}if(null!=e.policies)for(let[t,i]of Object.entries(e.policies)){null==this.#u[t]&&(this.#u[t]={sync:[],async:[]});let e=this.#u[t];null!=i.sync&&(e.sync=[...e.sync??[],...i.sync]),null!=i.async&&(e.async=[...e.async??[],...i.async])}}this.#l.info("engine initialized with {count} plugin(s)",{count:this.#d.length})}get did(){return this.#o.id}get identity(){return this.#o}get eventBus(){return this.#r}async dispose(){await Promise.all(this.#d.filter(e=>null!=e.dispose).map(e=>e.dispose?.()))}getGraphQLSource(e){let{graphID:t}=e,i=e.getViewerDID??(()=>this.#o.id);return{query:async e=>await this.queryGraph({id:t,text:e.text,variables:e.variables,viewerDID:i()??void 0}),mutate:async e=>await this.mutateGraph({id:t,text:e.text,variables:e.variables,viewerDID:i()??void 0}),subscribe:e=>this.#v({graphID:t,text:e.text,variables:e.variables,viewerDID:i()??void 0})}}registerContextFactory(e){this.#t.push(e)}async #D(){return await b(this.#i)}async #b(t){var i;let s,r,a,n,o=t.viewerDID??this.#o.id,u=this.#x(o),c=t.stores??this.#i,d={};for(let{name:e,factory:t}of this.#e)d[e]=t(u,c);for(let e of this.#t)d={...d,...e(u,c)};null!=t.contextExtensions&&(d={...d,...t.contextExtensions});let h=await b(c);try{s=await M(c)}catch{s=void 0}if(null!=s){let t=s;r={getUserModelAccessDefault:(e,t,i)=>h.getUserModelAccessDefault(e,t,i),isMemberOfAnyGroup:(e,i)=>t.isMemberOfAnyGroup(e,i),isMemberOfAnyCircle:(e,i)=>t.isMemberOfAnyCircle(e,i),getModelInterfaces:e=>h.getModelInterfaces(e),revocationChecker:e(I(t))};let[i,n]=await Promise.all([t.getGroupsForMember(o),t.getCirclesForMember(o)]);a={viewerDID:o,groupIDs:i.map(e=>e.id),circleIDs:n.map(e=>e.id),serverDefault:this.#s.read}}else r={getUserModelAccessDefault:(e,t,i)=>h.getUserModelAccessDefault(e,t,i),isMemberOfAnyGroup:async()=>!1,isMemberOfAnyCircle:async()=>!1,getModelInterfaces:e=>h.getModelInterfaces(e)},a={viewerDID:o,groupIDs:[],circleIDs:[],serverDefault:this.#s.read};let p=O({viewerDID:o,db:r,defaultAccessLevel:this.#s});return n={...l({store:(i={store:h,viewerDID:o,eventBus:this.#r,accessChecker:p,viewerReadAccess:a,contextExtensions:Object.keys(d).length>0?d:void 0}).store,viewerDID:i.viewerDID,events:i.eventBus,accessChecker:i.accessChecker,viewerReadAccess:i.viewerReadAccess}),...V},null!=i.contextExtensions?{...n,...i.contextExtensions}:n}#x(e){return{viewerDID:e}}async #I(e){return null==this.#a[e]&&(this.#a[e]=this.#D().then(async t=>{let i=await t.getGraph(e);if(null==i)throw this.#l.warn("graph {id} not found",{id:e}),delete this.#a[e],Error(`Graph not found: ${e}`);return this.#l.debug("cached model for graph {id}",{id:e}),{aliases:i.aliases,record:i.record,extensionSDL:i.extension_sdl??void 0,pluginConfig:i.plugin_config??void 0}})),await this.#a[e]}async getGraphQLSchema(e){return null==this.#m[e]&&(this.#m[e]=this.#I(e).then(t=>{let i;if(null!=t.pluginConfig){let e={},s={},r={},a={},n={};for(let[i,o]of Object.entries(t.pluginConfig)){let t=this.#c.get(i);if(null!=t){let i=t(o);if(Object.assign(e,i.resolvers.queryFields??{}),Object.assign(s,i.resolvers.mutationFields??{}),Object.assign(r,i.resolvers.subscriptionFields??{}),Object.assign(n,i.resolvers.nodeResolvers??{}),null!=i.resolvers.typeFields)for(let[e,t]of Object.entries(i.resolvers.typeFields))a[e]={...a[e],...t}}}i={queryFields:e,mutationFields:s,subscriptionFields:r,typeFields:a,nodeResolvers:n}}let s=u({...t,extensionResolvers:i});return this.#l.debug("cached schema for graph {id}",{id:e}),s}).catch(t=>{throw delete this.#m[e],t})),await this.#m[e]}async #M(e){let t=k(e.text),i=this.#A(t),s=await this.#C(i,e.viewerDID??this.#o.id);if(null!=s)return{result:s};let r=await this.getGraphQLSchema(e.graphID),a=R(r,t);return a.length>0?{result:{errors:a}}:{document:t,schema:r}}async #G(e,t,i){if(null==t.definitions[0])throw Error("Missing GraphQL document definition");return{schema:i,document:t,contextValue:await this.#b(e),variableValues:e.variables}}#A(e){let t=e.definitions[0];return null!=t&&t.kind===G.OPERATION_DEFINITION?t.operation:"query"}async #C(e,t){let i=await T(this.#u,e,{viewerDID:t});return"issues"in i&&null!=i.issues&&i.issues.length>0?{errors:i.issues.map(e=>new C(e.message,{extensions:null!=e.path?{path:e.path}:void 0}))}:null}async listGraphs(){let e=await this.#D();return{graphs:(await e.listGraphs()).map(e=>({id:e.id,name:e.name}))}}async loadGraph(e){let t=await this.#I(e.id);return{aliases:t.aliases,record:t.record,extensionSDL:t.extensionSDL}}async deployGraph(e){let t,i,s={};for(let t of e.clusters)Object.assign(s,v(t));let r=y.fromClusters({clusters:s});if(null!=e.plugins){i=e.plugins;let s=[];for(let[e,t]of Object.entries(i)){let i=this.#c.get(e);if(null==i)throw Error(`Plugin "${e}" not found or does not provide schema extensions`);let r=i(t);s.push(r.sdl)}s.length>0&&(t=s.join("\n"))}let a=await this.#D(),n=await a.createGraph({id:e.id??this.#p.getRandomID(),name:e.name,record:r.record,extensionSDL:t,pluginConfig:i}),o=r.toJSON();return this.#a[n]=Promise.resolve({...o,extensionSDL:t,pluginConfig:i}),delete this.#m[n],this.#l.info("deployed graph {id}",{id:n}),{id:n,...o}}async #k(e,t){let i="local"===t?"engine:mutation:authored":"engine:mutation:received",s=e.filter(e=>!e.dropped),r=new Map,a=[];for(let e of s){let t=function(e){if(null==e.document)throw Error("Cannot classify a dropped mutation result");return null==e.previousDoc?"create":null===e.document.data?"remove":"update"}(e);a.push(t);let i=e.document.id,s=r.get(i);null==s&&(s={docID:i,hasCreate:!1,hasUpdate:!1,hasRemove:!1,lastUpdateResult:null,lastRemoveResult:null,lastResult:e},r.set(i,s)),s.lastResult=e,"create"===t?s.hasCreate=!0:"update"===t?(s.hasUpdate=!0,s.lastUpdateResult=e):(s.hasRemove=!0,s.lastRemoveResult=e)}for(let e of r.values()){if(e.hasCreate&&e.hasRemove)continue;let t=e.lastResult.document;if(e.hasRemove){let t=e.lastRemoveResult??e.lastResult,i=t.document;await this.#r.emit("engine:document:removed",{documentID:i.id,viewerDID:t.authorDID});continue}if(e.hasCreate&&await this.#r.emit("engine:document:created",{documentID:t.id,modelID:t.model,viewerDID:e.lastResult.authorDID}),e.hasUpdate){let t=e.lastUpdateResult??e.lastResult,i=t.document;await this.#r.emit("engine:document:updated",{documentID:i.id,modelID:i.model,viewerDID:t.authorDID})}let i={type:e.hasCreate&&!e.hasUpdate?"create":"update",document:t,previous:{data:e.lastResult.previousDoc?.data??null},getCursor:()=>t.id};await this.#r.emit("document:saved",i)}for(let e=0;e<s.length;e++){let t=s[e],r=t.document;await this.#r.emit(i,{type:a[e],documentID:r.id,viewerDID:t.authorDID,mutationJWT:t.token,version:t.mutation.hlc,modelID:r.model})}}#E(e,t,i){return buildWriteAccessChecker({store:e,defaultAccessLevel:this.#s,extraTokensByIssuer:t,revocationChecker:i})}async #w(t,s){let r,a,{token:o,accessGate:l}=t,u=t.origin??"local",c=i(U,(await n(o)).payload),h=null!=s?await b(s):await this.#D(),m=s??this.#i,g=q(c.hlc)??Math.floor(Date.now()/1e3),v=new Map;try{a=await M(m);let t=await a.getHeldTokens({audience:c.iss,atTime:g});t.length>0&&v.set(c.iss,t.map(e=>e.token)),r=e(I(a))}catch{a=void 0}let w={store:h,validators:this.#g,hlc:this.#n,checkWriteAccess:this.#E(h,v,r),postStateGate:l},y=await h.getDocument(d.fromString(c.sub));if(null!=a&&null!=y&&y.owner!==c.iss){let e=await L({p2pStore:a,graphStore:h,selfDID:this.did,iss:c.iss,mutationHLC:c.hlc,doc:y,arrivalGroupID:t.arrivalGroupID});if(!e.ok){if(this.#l.warn("mutation denied: issuer removed from group",{iss:c.iss,docID:y.id,mutationHLC:c.hlc,removedGroupID:e.removedGroupID,removedAtHLC:e.removedAtHLC,origin:u}),"peer"===u)return{document:null,previousDoc:y,mutation:c,authorDID:c.iss,hash:F(o),token:o,dropped:!0};throw new f(y.id)}}let D=await p(w,c),x=F(o),A=c.iss;if(null==D)return{document:null,previousDoc:y,mutation:c,authorDID:A,hash:x,token:o,dropped:!0};await h.insertMutationLogEntry({mutation_hash:x,model_id:D.model,document_id:D.id,author_did:A,hlc:c.hlc,mutation_jwt:o,status:"applied"});let C={document:D,previousDoc:y,mutation:c,authorDID:A,hash:x,token:o,dropped:!1};return null==s&&await this.#k([C],u),C}async #y(t){let{tokens:s,accessGate:r}=t,a=t.origin??"local",o=await Promise.all(s.map(async e=>{let t=i(U,(await n(e)).payload);return{token:e,mutation:t}})),l=[];return await this.#i.withTransaction(async t=>{let i,s,n=await t.getStore(D),u=new Map,c=new Map,h=new Set,m=Math.floor(Date.now()/1e3);for(let{mutation:e}of o){let t=q(e.hlc);if(null==t){h.add(e.iss);continue}let i=c.get(e.iss);(null==i||t<i)&&c.set(e.iss,t)}for(let r of new Set(o.map(e=>e.mutation.iss))){let a=c.get(r),n=h.has(r)||null==a?Math.min(a??m,m):a;try{let a=await M(t);s=a;let o=await a.getHeldTokens({audience:r,atTime:n});o.length>0&&u.set(r,o.map(e=>e.token)),i??=e(I(a))}catch{}}let g=this.#E(n,u,i);for(let{token:e,mutation:t}of o){let i={store:n,validators:this.#g,hlc:this.#n,checkWriteAccess:g,postStateGate:r},o=await n.getDocument(d.fromString(t.sub));if(null!=s&&null!=o&&o.owner!==t.iss){let e=await L({p2pStore:s,graphStore:n,selfDID:this.did,iss:t.iss,mutationHLC:t.hlc,doc:o,arrivalGroupID:void 0});if(!e.ok)throw this.#l.warn("mutation denied: issuer removed from group",{iss:t.iss,docID:o.id,mutationHLC:t.hlc,removedGroupID:e.removedGroupID,removedAtHLC:e.removedAtHLC,origin:a}),new f(o.id)}let u=await p(i,t),c=F(e);if(null==u){l.push({document:null,previousDoc:o,mutation:t,authorDID:t.iss,hash:c,token:e,dropped:!0});continue}await n.insertMutationLogEntry({mutation_hash:c,model_id:u.model,document_id:u.id,author_did:t.iss,hlc:t.hlc,mutation_jwt:e,status:"applied"}),l.push({document:u,previousDoc:o,mutation:t,authorDID:t.iss,hash:c,token:e,dropped:!1})}}),await this.#k(l,a),{results:l,dropped:l.filter(e=>e.dropped).length}}async #f(e){let t=await this.#M(e);if("result"in t)return t.result;let i=await this.#G(e,t.document,t.schema);return await A(i)}async #v(e){let t=await this.#M(e);if("result"in t)return t.result;let i=await this.#G(e,t.document,t.schema);return await E(i)}async queryGraph(e){let t=k(e.text);if("mutation"===this.#A(t))throw Error("queryGraph() does not accept mutation operations. Use mutateGraph() instead.");return await this.#f({graphID:e.id,text:e.text,variables:e.variables??{},viewerDID:e.viewerDID})}async mutateGraph(e){if(!r(this.#o))throw Error("mutateGraph requires a SigningIdentity");let t=this.#o,i=e.id;await this.getGraphQLSchema(i);let s=[],n=await this.#i.withTransaction(async r=>{let n=async e=>{let i=a(await t.signToken(e)),n=await this.#w({token:i},r);if(s.push(n),null==n.document)throw Error("Unexpected dropped mutation in mutateGraph (no gate configured)");return n.document},o=Math.floor(Date.now()/1e3),l=new Set(e.delegationTokens??[]);try{let e=await M(r);for(let i of(await e.getHeldTokens({audience:t.id,atTime:o})))l.add(i.token)}catch{}let u=l.size>0?Array.from(l):void 0,c=g({issuer:t.id,hlc:this.#n,getRandomValues:this.#p.getRandomValues,cap:u,processSetMutation:e=>n(e),processChangeMutation:e=>n(e)});return await this.#f({graphID:i,text:e.text,variables:e.variables??{},viewerDID:e.viewerDID??t.id,stores:r,contextExtensions:{executeCreateMutation:async e=>await c.createDocument({modelID:e.modelID,data:e.data}),executeSetMutation:async e=>await c.setDocument({modelID:e.modelID,unique:e.unique,data:e.data}),executeUpdateMutation:async e=>await c.updateDocument({docID:e.input.id,patch:m(e.input.patch)}),executeRemoveMutation:async e=>{await c.removeDocument({docID:e.id})}}})});return await this.#k(s,"local"),n}async subscribeToGraph(e){return await this.#v({graphID:e.id,text:e.text,variables:e.variables??{},viewerDID:e.viewerDID})}async getAPI(e){return await this.#h.getAPI(e)}}
|
|
1
|
+
import{createRevocationChecker as e}from"@enkaku/capability";import{createRuntime as t}from"@enkaku/runtime";import{asType as i,createValidator as s}from"@enkaku/schema";import{isSigningIdentity as r,stringifyToken as a,verifyToken as n}from"@enkaku/token";import{KubunDB as o}from"@kubun/db";import{createReadContext as l,createSchema as u}from"@kubun/graphql";import{HLC as c}from"@kubun/hlc";import{DocumentID as d}from"@kubun/id";import{getKubunLogger as h}from"@kubun/logger";import{applyMutation as p,convertPatchInput as m,createMutationOperations as g,WriteAccessDeniedError as f}from"@kubun/mutation";import{clusterToRecord as w,documentMutation as v,GraphModel as D}from"@kubun/protocol";import{GRAPH_STORE as y,getGraphStore as b,graphStoreDefinition as x}from"@kubun/store-graph";import{createP2PRevocationBackend as M,getP2PStore as I}from"@kubun/store-p2p";import{execute as C,GraphQLError as A,Kind as G,parse as k,subscribe as E,validate as S}from"graphql";import{createAccessChecker as R}from"./access-control.js";import{EngineEventBus as L}from"./events.js";import{checkMembership as F}from"./membership-check.js";import{computeMutationHash as O}from"./mutation-hash.js";import{runPolicies as T}from"./policies.js";import{createRegistry as j}from"./registry.js";let P=s(v);function U(e){if(null==e)return;let t=c.parse(e).wallTime;return Number.isFinite(t)?Math.floor(t/1e3):void 0}export function buildWriteAccessChecker(e){let{store:t,defaultAccessLevel:i,extraTokensByIssuer:s,revocationChecker:r}=e,a=new Map,n={getUserModelAccessDefault:(e,i,s)=>t.getUserModelAccessDefault(e,i,s),isMemberOfAnyGroup:async()=>!1,isMemberOfAnyCircle:async()=>!1,getModelInterfaces:async e=>{let i=a.get(e);if(null!=i)return i;let s=await t.getModelInterfaces(e);return a.set(e,s),s},revocationChecker:r};return(e,t)=>{var r;let a=(null==(r=t.cap)?void 0:Array.isArray(r)?r:[r])??[],o=s?.get(t.iss)??[],l=0===a.length&&0===o.length?void 0:[...a,...o];return R({viewerDID:t.iss,delegationTokens:l,db:n,defaultAccessLevel:i,atTime:U(t.hlc)})(e,"write")}}function q(){throw Error("Graph mutations require mutateGraph() — the core context does not sign or log writes. Use KubunEngine.mutateGraph() instead.")}function H(){throw Error("Access control mutations are not supported by the engine core — plugin override required.")}function B(){throw Error("Transaction mutations are not supported by the engine core — plugin override required (see plugin-rpc).")}let _={executeCreateMutation:q,executeSetMutation:q,executeUpdateMutation:q,executeRemoveMutation:q,executeSetModelAccessDefaults:H,executeRemoveModelAccessDefaults:H,executeSetDocumentAccessOverride:H,executeRemoveDocumentAccessOverride:H,beginTransaction:B,commitTransaction:B,rollbackTransaction:B};export class KubunEngine{#e=[];#t=[];#i;#s;#r;#a={};#n;#o;#l;#u;#c;#d={};#h=new Map;#p=[];#m;#g;#f={};#w={};constructor(e){this.#i=e.db instanceof o?e.db:new o({adapter:e.db}),this.#i.register(x),this.#l=e.identity,this.#s=e.defaultAccessLevel??{read:"anyone",write:"only_owner"},this.#n=new c({nodeID:e.identity.id}),this.#u=e.maxDriftMS??36e5,this.#c=e.logger??h("engine"),this.#r=e.eventBus??new L({logger:this.#c.getChild("events")}),this.#g=t(e.runtime),this.#m=j();let i=e.plugins??[],s={engine:this,graph:{execute:e=>this.#v(e),subscribe:e=>this.#D(e),applyVerifiedMutation:e=>this.#y(e),applyVerifiedMutations:e=>this.#b(e)},db:this.#i,runtime:this.#g,identity:this.#l,eventBus:this.#r,hlc:this.#n,getLogger:e=>this.#c.getChild(e)};for(let e of i){let t=e(s);this.#p.push(t),null!=t.api&&this.#m.registerPlugin(t.name,t.api)}this.#m.closeGate();let r=new Set;for(let e of this.#p){if(null!=e.schemaExtension&&this.#h.set(e.name,e.schemaExtension),null!=e.createContextFactory){if(r.has(e.name))throw Error(`Duplicate plugin namespace: ${e.name}`);r.add(e.name),this.#e.push({name:e.name,factory:e.createContextFactory()})}if(null!=e.policies)for(let[t,i]of Object.entries(e.policies)){null==this.#d[t]&&(this.#d[t]={sync:[],async:[]});let e=this.#d[t];null!=i.sync&&(e.sync=[...e.sync??[],...i.sync]),null!=i.async&&(e.async=[...e.async??[],...i.async])}}this.#c.info("engine initialized with {count} plugin(s)",{count:this.#p.length})}get did(){return this.#l.id}get identity(){return this.#l}get eventBus(){return this.#r}async dispose(){await Promise.all(this.#p.filter(e=>null!=e.dispose).map(e=>e.dispose?.()))}getGraphQLSource(e){let{graphID:t}=e,i=e.getViewerDID??(()=>this.#l.id);return{query:async e=>await this.queryGraph({id:t,text:e.text,variables:e.variables,viewerDID:i()??void 0}),mutate:async e=>await this.mutateGraph({id:t,text:e.text,variables:e.variables,viewerDID:i()??void 0}),subscribe:e=>this.#D({graphID:t,text:e.text,variables:e.variables,viewerDID:i()??void 0})}}registerContextFactory(e){this.#t.push(e)}async #x(){return await b(this.#i)}async #M(e){return null!=this.#o||(this.#o=(async()=>{let t=null!=e?await b(e):await this.#x(),i=await t.getMaxMutationHLC();if(null!=i){let e=c.parse(i),t=Date.now()+this.#u,s=Math.min(e.wallTime,t);this.#n.receive({...e,wallTime:s})}})()),await this.#o}async #I(t){var i;let s,r,a,n,o=t.viewerDID??this.#l.id,u=this.#C(o),c=t.stores??this.#i,d={};for(let{name:e,factory:t}of this.#e)d[e]=t(u,c);for(let e of this.#t)d={...d,...e(u,c)};null!=t.contextExtensions&&(d={...d,...t.contextExtensions});let h=await b(c);try{s=await I(c)}catch{s=void 0}if(null!=s){let t=s;r={getUserModelAccessDefault:(e,t,i)=>h.getUserModelAccessDefault(e,t,i),isMemberOfAnyGroup:(e,i)=>t.isMemberOfAnyGroup(e,i),isMemberOfAnyCircle:(e,i)=>t.isMemberOfAnyCircle(e,i),getModelInterfaces:e=>h.getModelInterfaces(e),revocationChecker:e(M(t))};let[i,n]=await Promise.all([t.getGroupsForMember(o),t.getCirclesForMember(o)]);a={viewerDID:o,groupIDs:i.map(e=>e.id),circleIDs:n.map(e=>e.id),serverDefault:this.#s.read}}else r={getUserModelAccessDefault:(e,t,i)=>h.getUserModelAccessDefault(e,t,i),isMemberOfAnyGroup:async()=>!1,isMemberOfAnyCircle:async()=>!1,getModelInterfaces:e=>h.getModelInterfaces(e)},a={viewerDID:o,groupIDs:[],circleIDs:[],serverDefault:this.#s.read};let p=R({viewerDID:o,db:r,defaultAccessLevel:this.#s});return n={...l({store:(i={store:h,viewerDID:o,eventBus:this.#r,accessChecker:p,viewerReadAccess:a,contextExtensions:Object.keys(d).length>0?d:void 0}).store,viewerDID:i.viewerDID,events:i.eventBus,accessChecker:i.accessChecker,viewerReadAccess:i.viewerReadAccess}),..._},null!=i.contextExtensions?{...n,...i.contextExtensions}:n}#C(e){return{viewerDID:e}}async #A(e){return null==this.#a[e]&&(this.#a[e]=this.#x().then(async t=>{let i=await t.getGraph(e);if(null==i)throw this.#c.warn("graph {id} not found",{id:e}),delete this.#a[e],Error(`Graph not found: ${e}`);return this.#c.debug("cached model for graph {id}",{id:e}),{aliases:i.aliases,record:i.record,extensionSDL:i.extension_sdl??void 0,pluginConfig:i.plugin_config??void 0}})),await this.#a[e]}async getGraphQLSchema(e){return null==this.#f[e]&&(this.#f[e]=this.#A(e).then(t=>{let i;if(null!=t.pluginConfig){let e={},s={},r={},a={},n={};for(let[i,o]of Object.entries(t.pluginConfig)){let t=this.#h.get(i);if(null!=t){let i=t(o);if(Object.assign(e,i.resolvers.queryFields??{}),Object.assign(s,i.resolvers.mutationFields??{}),Object.assign(r,i.resolvers.subscriptionFields??{}),Object.assign(n,i.resolvers.nodeResolvers??{}),null!=i.resolvers.typeFields)for(let[e,t]of Object.entries(i.resolvers.typeFields))a[e]={...a[e],...t}}}i={queryFields:e,mutationFields:s,subscriptionFields:r,typeFields:a,nodeResolvers:n}}let s=u({...t,extensionResolvers:i});return this.#c.debug("cached schema for graph {id}",{id:e}),s}).catch(t=>{throw delete this.#f[e],t})),await this.#f[e]}async #G(e){let t=k(e.text),i=this.#k(t),s=await this.#E(i,e.viewerDID??this.#l.id);if(null!=s)return{result:s};let r=await this.getGraphQLSchema(e.graphID),a=S(r,t);return a.length>0?{result:{errors:a}}:{document:t,schema:r}}async #S(e,t,i){if(null==t.definitions[0])throw Error("Missing GraphQL document definition");return{schema:i,document:t,contextValue:await this.#I(e),variableValues:e.variables}}#k(e){let t=e.definitions[0];return null!=t&&t.kind===G.OPERATION_DEFINITION?t.operation:"query"}async #E(e,t){let i=await T(this.#d,e,{viewerDID:t});return"issues"in i&&null!=i.issues&&i.issues.length>0?{errors:i.issues.map(e=>new A(e.message,{extensions:null!=e.path?{path:e.path}:void 0}))}:null}async listGraphs(){let e=await this.#x();return{graphs:(await e.listGraphs()).map(e=>({id:e.id,name:e.name}))}}async loadGraph(e){let t=await this.#A(e.id);return{aliases:t.aliases,record:t.record,extensionSDL:t.extensionSDL}}async deployGraph(e){let t,i,s={};for(let t of e.clusters)Object.assign(s,w(t));let r=D.fromClusters({clusters:s});if(null!=e.plugins){i=e.plugins;let s=[];for(let[e,t]of Object.entries(i)){let i=this.#h.get(e);if(null==i)throw Error(`Plugin "${e}" not found or does not provide schema extensions`);let r=i(t);s.push(r.sdl)}s.length>0&&(t=s.join("\n"))}let a=await this.#x(),n=await a.createGraph({id:e.id??this.#g.getRandomID(),name:e.name,record:r.record,extensionSDL:t,pluginConfig:i});delete this.#a[n],delete this.#f[n],this.#c.info("deployed graph {id}",{id:n});let o=await this.#A(n);return{id:n,aliases:o.aliases,record:o.record}}async #R(e,t){let i="local"===t?"engine:mutation:authored":"engine:mutation:received",s=e.filter(e=>!e.dropped),r=new Map,a=[];for(let e of s){let t=function(e){if(null==e.document)throw Error("Cannot classify a dropped mutation result");return null==e.previousDoc?"create":null===e.document.data?"remove":"update"}(e);a.push(t);let i=e.document.id,s=r.get(i);null==s&&(s={docID:i,hasCreate:!1,hasUpdate:!1,hasRemove:!1,lastUpdateResult:null,lastRemoveResult:null,lastResult:e},r.set(i,s)),s.lastResult=e,"create"===t?s.hasCreate=!0:"update"===t?(s.hasUpdate=!0,s.lastUpdateResult=e):(s.hasRemove=!0,s.lastRemoveResult=e)}for(let e of r.values()){if(e.hasCreate&&e.hasRemove)continue;let t=e.lastResult.document;if(e.hasRemove){let t=e.lastRemoveResult??e.lastResult,i=t.document;await this.#r.emit("engine:document:removed",{documentID:i.id,viewerDID:t.authorDID});continue}if(e.hasCreate&&await this.#r.emit("engine:document:created",{documentID:t.id,modelID:t.model,viewerDID:e.lastResult.authorDID}),e.hasUpdate){let t=e.lastUpdateResult??e.lastResult,i=t.document;await this.#r.emit("engine:document:updated",{documentID:i.id,modelID:i.model,viewerDID:t.authorDID})}let i={type:e.hasCreate&&!e.hasUpdate?"create":"update",document:t,previous:{data:e.lastResult.previousDoc?.data??null},getCursor:()=>t.id};await this.#r.emit("document:saved",i)}for(let e=0;e<s.length;e++){let t=s[e],r=t.document;await this.#r.emit(i,{type:a[e],documentID:r.id,viewerDID:t.authorDID,mutationJWT:t.token,version:t.mutation.hlc,modelID:r.model})}}#L(e,t,i){return buildWriteAccessChecker({store:e,defaultAccessLevel:this.#s,extraTokensByIssuer:t,revocationChecker:i})}async #y(t,s){let r,a,{token:o,accessGate:l}=t,u=t.origin??"local";await this.#M(s);let h=i(P,(await n(o)).payload),m=null!=s?await b(s):await this.#x(),g=s??this.#i,w=U(h.hlc)??Math.floor(Date.now()/1e3),v=new Map;try{a=await I(g);let t=await a.getHeldTokens({audience:h.iss,atTime:w});t.length>0&&v.set(h.iss,t.map(e=>e.token)),r=e(M(a))}catch{a=void 0}let D={store:m,validators:this.#w,hlc:this.#n,maxDriftMS:this.#u,checkWriteAccess:this.#L(m,v,r),postStateGate:l},y=await m.getDocument(d.fromString(h.sub));if(null!=a&&null!=y&&y.owner!==h.iss){let e=await F({p2pStore:a,graphStore:m,selfDID:this.did,iss:h.iss,mutationHLC:h.hlc,doc:y,arrivalGroupID:t.arrivalGroupID});if(!e.ok){if(this.#c.warn("mutation denied: issuer removed from group",{iss:h.iss,docID:y.id,mutationHLC:h.hlc,removedGroupID:e.removedGroupID,removedAtHLC:e.removedAtHLC,origin:u}),"peer"===u)return{document:null,previousDoc:y,mutation:h,authorDID:h.iss,hash:O(o),token:o,dropped:!0};throw new f(y.id)}}let x=await p(D,h);"local"!==u&&this.#n.receive(c.parse(h.hlc));let C=O(o),A=h.iss;if(null==x)return{document:null,previousDoc:y,mutation:h,authorDID:A,hash:C,token:o,dropped:!0};await m.insertMutationLogEntry({mutation_hash:C,model_id:x.model,document_id:x.id,author_did:A,hlc:h.hlc,mutation_jwt:o,status:"applied"});let G={document:x,previousDoc:y,mutation:h,authorDID:A,hash:C,token:o,dropped:!1};return null==s&&await this.#R([G],u),G}async #b(t){let{tokens:s,accessGate:r}=t,a=t.origin??"local";await this.#M();let o=await Promise.all(s.map(async e=>{let t=i(P,(await n(e)).payload);return{token:e,mutation:t}})),l=[];return await this.#i.withTransaction(async t=>{let i,s,n=await t.getStore(y),u=new Map,h=new Map,m=new Set,g=Math.floor(Date.now()/1e3);for(let{mutation:e}of o){let t=U(e.hlc);if(null==t){m.add(e.iss);continue}let i=h.get(e.iss);(null==i||t<i)&&h.set(e.iss,t)}for(let r of new Set(o.map(e=>e.mutation.iss))){let a=h.get(r),n=m.has(r)||null==a?Math.min(a??g,g):a;try{let a=await I(t);s=a;let o=await a.getHeldTokens({audience:r,atTime:n});o.length>0&&u.set(r,o.map(e=>e.token)),i??=e(M(a))}catch{}}let w=this.#L(n,u,i);for(let{token:e,mutation:t}of o){let i={store:n,validators:this.#w,hlc:this.#n,maxDriftMS:this.#u,checkWriteAccess:w,postStateGate:r},o=await n.getDocument(d.fromString(t.sub));if(null!=s&&null!=o&&o.owner!==t.iss){let e=await F({p2pStore:s,graphStore:n,selfDID:this.did,iss:t.iss,mutationHLC:t.hlc,doc:o,arrivalGroupID:void 0});if(!e.ok)throw this.#c.warn("mutation denied: issuer removed from group",{iss:t.iss,docID:o.id,mutationHLC:t.hlc,removedGroupID:e.removedGroupID,removedAtHLC:e.removedAtHLC,origin:a}),new f(o.id)}let u=await p(i,t);"local"!==a&&this.#n.receive(c.parse(t.hlc));let h=O(e);if(null==u){l.push({document:null,previousDoc:o,mutation:t,authorDID:t.iss,hash:h,token:e,dropped:!0});continue}await n.insertMutationLogEntry({mutation_hash:h,model_id:u.model,document_id:u.id,author_did:t.iss,hlc:t.hlc,mutation_jwt:e,status:"applied"}),l.push({document:u,previousDoc:o,mutation:t,authorDID:t.iss,hash:h,token:e,dropped:!1})}}),await this.#R(l,a),{results:l,dropped:l.filter(e=>e.dropped).length}}async #v(e){let t=await this.#G(e);if("result"in t)return t.result;let i=await this.#S(e,t.document,t.schema);return await C(i)}async #D(e){let t=await this.#G(e);if("result"in t)return t.result;let i=await this.#S(e,t.document,t.schema);return await E(i)}async queryGraph(e){let t=k(e.text);if("mutation"===this.#k(t))throw Error("queryGraph() does not accept mutation operations. Use mutateGraph() instead.");return await this.#v({graphID:e.id,text:e.text,variables:e.variables??{},viewerDID:e.viewerDID})}async mutateGraph(e){if(!r(this.#l))throw Error("mutateGraph requires a SigningIdentity");let t=this.#l,i=e.id;await this.getGraphQLSchema(i);let s=[],n=await this.#i.withTransaction(async r=>{let n=async e=>{let i=a(await t.signToken(e)),n=await this.#y({token:i},r);if(s.push(n),null==n.document)throw Error("Unexpected dropped mutation in mutateGraph (no gate configured)");return n.document},o=Math.floor(Date.now()/1e3),l=new Set(e.delegationTokens??[]);try{let e=await I(r);for(let i of(await e.getHeldTokens({audience:t.id,atTime:o})))l.add(i.token)}catch{}let u=l.size>0?Array.from(l):void 0;await this.#M(r);let c=g({issuer:t.id,hlc:this.#n,getRandomValues:this.#g.getRandomValues,cap:u,processSetMutation:e=>n(e),processChangeMutation:e=>n(e)});return await this.#v({graphID:i,text:e.text,variables:e.variables??{},viewerDID:e.viewerDID??t.id,stores:r,contextExtensions:{executeCreateMutation:async e=>await c.createDocument({modelID:e.modelID,data:e.data}),executeSetMutation:async e=>await c.setDocument({modelID:e.modelID,unique:e.unique,data:e.data}),executeUpdateMutation:async e=>await c.updateDocument({docID:e.input.id,patch:m(e.input.patch)}),executeRemoveMutation:async e=>{await c.removeDocument({docID:e.id})}}})});return await this.#R(s,"local"),n}async subscribeToGraph(e){return await this.#D({graphID:e.id,text:e.text,variables:e.variables??{},viewerDID:e.viewerDID})}async getAPI(e){return await this.#m.getAPI(e)}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/engine",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"type": "module",
|
|
@@ -22,22 +22,22 @@
|
|
|
22
22
|
"@enkaku/schema": "^0.16.0",
|
|
23
23
|
"@enkaku/token": "^0.16.0",
|
|
24
24
|
"@noble/hashes": "^2.2.0",
|
|
25
|
-
"graphql": "^16.14.
|
|
26
|
-
"@kubun/
|
|
25
|
+
"graphql": "^16.14.2",
|
|
26
|
+
"@kubun/graphql": "^0.10.1",
|
|
27
27
|
"@kubun/db-adapter": "^0.10.0",
|
|
28
28
|
"@kubun/logger": "^0.10.0",
|
|
29
|
+
"@kubun/mutation": "^0.10.0",
|
|
30
|
+
"@kubun/protocol": "^0.10.0",
|
|
31
|
+
"@kubun/hlc": "^0.10.0",
|
|
29
32
|
"@kubun/store-graph": "^0.10.1",
|
|
30
33
|
"@kubun/store-p2p": "^0.10.0",
|
|
31
|
-
"@kubun/
|
|
32
|
-
"@kubun/hlc": "^0.10.0",
|
|
33
|
-
"@kubun/mutation": "^0.10.0",
|
|
34
|
-
"@kubun/protocol": "^0.10.0"
|
|
34
|
+
"@kubun/db": "^0.10.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@testcontainers/postgresql": "^12.0.1",
|
|
38
38
|
"@kubun/db-postgres": "^0.10.0",
|
|
39
|
-
"@kubun/
|
|
40
|
-
"@kubun/
|
|
39
|
+
"@kubun/test-utils": "^0.10.0",
|
|
40
|
+
"@kubun/id": "^0.10.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build:clean": "del lib",
|