@north-light/cli 0.3.483 → 0.3.484
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/dist/api/client.d.ts +6 -0
- package/dist/api/client.js +3 -3
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/custom-objects.d.ts +5 -1
- package/dist/api/dto/docs.d.ts +26 -0
- package/dist/api/dto/objects.d.ts +21 -1
- package/dist/api/dto/plugins.d.ts +22 -0
- package/dist/api/dto/plugins.js +0 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -1
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.js +1 -1
- package/dist/builtin-memory/crouter-plugin/deploying.md +107 -0
- package/dist/builtin-memory/crouter-sdk/release-notes.md +69 -0
- package/dist/clients/attach/viewer.js +579 -578
- package/dist/commands/pkg/plugin-manage.d.ts +4 -0
- package/dist/commands/pkg/plugin-manage.js +18 -16
- package/dist/core/canvas/migrations.js +27 -18
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +1 -1
- package/dist/core/command-plugins/caller-header.d.ts +10 -0
- package/dist/core/command-plugins/caller-header.js +1 -0
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/endpoint.d.ts +4 -0
- package/dist/core/command-plugins/endpoint.js +1 -1
- package/dist/core/command-plugins/transport/exec-invoke-async.d.ts +11 -0
- package/dist/core/command-plugins/transport/exec-invoke-async.js +1 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +14 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +1 -1
- package/dist/core/command-plugins/transport/http-invoke.d.ts +9 -2
- package/dist/core/command-plugins/transport/http-invoke.js +3 -3
- package/dist/core/command-plugins/transport/http-plan.js +1 -1
- package/dist/core/command-plugins/transport/sign.d.ts +55 -0
- package/dist/core/command-plugins/transport/sign.js +3 -0
- package/dist/core/graph/access.d.ts +19 -0
- package/dist/core/graph/access.js +3 -3
- package/dist/core/graph/custom.d.ts +29 -9
- package/dist/core/graph/custom.js +9 -8
- package/dist/core/graph/events.js +8 -8
- package/dist/core/graph/package-kinds.d.ts +25 -0
- package/dist/core/graph/package-kinds.js +1 -0
- package/dist/core/graph/package-reconcile.d.ts +20 -0
- package/dist/core/graph/package-reconcile.js +1 -0
- package/dist/core/graph/reader.d.ts +5 -0
- package/dist/core/graph/reader.js +3 -3
- package/dist/core/graph/receivers.d.ts +2 -1
- package/dist/core/graph/types.d.ts +2 -0
- package/dist/core/graph/watches.d.ts +4 -0
- package/dist/core/graph/watches.js +9 -8
- package/dist/core/peer/directory.d.ts +2 -1
- package/dist/core/peer/directory.js +1 -1
- package/dist/core/peer/ingest.d.ts +6 -1
- package/dist/core/peer/ingest.js +1 -1
- package/dist/core/peer/names.d.ts +2 -0
- package/dist/core/peer/names.js +1 -1
- package/dist/core/peer/outbox-store.d.ts +2 -0
- package/dist/core/peer/receiver.js +1 -1
- package/dist/core/peer/stubs.d.ts +26 -1
- package/dist/core/peer/stubs.js +1 -1
- package/dist/core/plugin-app.d.ts +31 -0
- package/dist/core/plugin-app.js +1 -0
- package/dist/core/plugin-objects-manifest.d.ts +35 -0
- package/dist/core/plugin-objects-manifest.js +1 -0
- package/dist/core/providers/bundles.js +2 -2
- package/dist/core/providers/call.d.ts +3 -0
- package/dist/core/providers/call.js +1 -1
- package/dist/core/secrets.d.ts +11 -0
- package/dist/core/secrets.js +1 -1
- package/dist/daemon/api/app-listener.js +13 -13
- package/dist/daemon/api/handlers/custom-objects.js +3 -3
- package/dist/daemon/api/handlers/objects.js +3 -2
- package/dist/daemon/api/handlers/package-docs.js +1 -1
- package/dist/daemon/api/handlers/plugin-app-token.d.ts +6 -0
- package/dist/daemon/api/handlers/plugin-app-token.js +1 -0
- package/dist/daemon/api/handlers/providers.d.ts +7 -0
- package/dist/daemon/api/handlers/providers.js +1 -1
- package/dist/daemon/api/operations.js +1 -1
- package/dist/daemon/api/server.js +1 -1
- package/dist/daemon/crtrd.js +7 -5
- package/dist/daemon/peer/forward.d.ts +4 -1
- package/dist/daemon/peer/forward.js +1 -1
- package/dist/daemon/plugin-objects/read-leaf.d.ts +20 -0
- package/dist/daemon/plugin-objects/read-leaf.js +2 -0
- package/dist/types.d.ts +16 -0
- package/docs/plugin/deploying.md +107 -0
- package/docs/sdk/release-notes.md +69 -0
- package/package.json +2 -2
- package/packages/crouter-identity/dist/index.d.ts +1 -1
- package/packages/crouter-identity/dist/index.js +6 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
var P=Object.defineProperty;var p=(e,r)=>P(e,"name",{value:r,configurable:!0});import{Parser as J}from"tar";import{createHash as I}from"node:crypto";import{chmodSync as W,renameSync as k}from"node:fs";import{dirname as $,join as w,resolve as V,sep as z}from"node:path";import{validateCommandManifest as B}from"../../api/command-manifest/manifest.js";import{ensureScopeInitialized as F,invalidPluginKindsReasons as M,readConfig as R,updateConfig as D,updateState as C}from"../config.js";import{general as x}from"../errors.js";import{atomicWriteJson as T,atomicWriteText as q,ensureDir as j,nowIso as E,pathExists as _,removePath as S,syncDirectory as U,syncTree as H}from"../fs-utils.js";import{invalidPageComponentsReasons as K,normalizePageComponents as G}from"../human/page-catalog.js";import{validateMemoryExtensionDeclarations as X}from"../plugin-extensions.js";import{validateBundleTransport as Z}from"./endpoint.js";const O=/^[A-Za-z0-9_-]{1,64}$/,L=new Set(["id","major","groups"]),Y=new Set(["group","consent_line","rate","who","allowlist"]),Q=new Set(["own","user","allowlist","any"]),ee=new Set(["bundleVersion","kinds","page_components","memory_extensions","version","transport","provider"]);function ne(e,r,n,o,s,a){return{code:e,message:r,received:n,expected:o,next:s,...a!==void 0?{path:a}:{}}}p(ne,"issue");function d(e,r,n,o,s){return ne("plugin_bundle_invalid",e,r,n,o,s)}p(d,"bundleInvalid");function re(e){if(e[0]===31&&e[1]===139)return d("bundle archive must be uncompressed tar data","gzip-compressed archive","an uncompressed tar archive","Serve the archive without compression.");if(e.length===0||e.length%512!==0)return d("bundle archive must be a non-empty whole number of 512-byte tar blocks",`${e.length} bytes`,"a non-empty tar stream with a 512-byte block length","Serve one complete uncompressed tar archive.");if(e.length<1024||!e.subarray(-1024).every(r=>r===0))return d("bundle archive is missing the required two zero-block terminator","archive tail is not 1024 zero bytes","a complete tar stream ending in two 512-byte zero blocks","Serve the complete uncompressed tar archive.")}p(re,"archiveEnvelopeIssue");function oe(e,r){if(e.length===0||e.includes("\0")||e.includes("\\")||e.startsWith("/")||/^[A-Za-z]:($|\/)/.test(e))return d("bundle member path must be a safe relative POSIX path",e,"a relative path without absolute, drive-letter, backslash, or NUL forms","Use a safe relative POSIX member path.",e);if(r==="file"&&e.endsWith("/"))return d("only directory members may have a trailing slash",e,"a regular-file path without a trailing slash","Emit this member as a regular file path.",e);if((r==="directory"&&e.endsWith("/")?e.slice(0,-1):e).split("/").some(o=>o===""||o==="."||o===".."))return d("bundle member path contains an unsafe segment",e,"non-empty segments other than . or ..","Use a safe relative POSIX member path.",e)}p(oe,"validateMemberName");function se(e,r){if(r==="file")return e==="bundle.json"||e==="commands.json"||e==="hooks.json"||e.startsWith("memory/")&&e.endsWith(".md")||e.startsWith("hooks/")?void 0:d("bundle contains an unsupported file member",e,"bundle.json, commands.json, hooks.json, memory/**/*.md, or hooks/**","Remove unsupported members from the archive.",e);if(!(e==="memory"||e.startsWith("memory/")||e==="hooks"||e.startsWith("hooks/")))return d("bundle contains an unsupported directory member",e,"memory or hooks, or a directory below either subtree","Remove unsupported directory members from the archive.",e)}p(se,"validateMemberShape");function te(e){return e.type==="directory"&&e.path.endsWith("/")?e.path.slice(0,-1):e.path}p(te,"materializedPath");function ie(e){const r=new Map;for(const n of e){const o=te(n),s=r.get(o);if(s!==void 0)return d(s===n.type?"bundle contains a duplicate member":"bundle claims one path as both a file and a directory",`${o} (${s} and ${n.type})`,"each materialized path exactly once, with one type","Emit each archive path once, as a single member type.",o);r.set(o,n.type)}for(const[n,o]of r)if(o==="file"){for(const s of r.keys())if(s.startsWith(`${n}/`))return d("bundle nests a member beneath a regular-file path",`${s} beneath file ${n}`,"members nested only beneath directory paths","Emit the parent path as a directory or remove the nested member.",s)}}p(ie,"validateMemberTree");async function ae(e){const r=[],n=new Set;let o;return await new Promise((s,a)=>{const l=new J({strict:!0,maxMetaEntrySize:Number.MAX_SAFE_INTEGER,gzip:!1,brotli:!1,zstd:!1,onReadEntry:p(t=>{const m=t.path,f=t.type==="File"||t.type==="OldFile"?"file":t.type==="Directory"?"directory":void 0;if(f===void 0||t.unsupported||t.ignore||t.linkpath!==void 0){o??=d("bundle member must be a regular file or directory without a link target",`${m} (${t.type})`,"a regular file or directory member without link metadata","Remove links and unsupported member types from the archive.",m),t.resume();return}o??=oe(m,f),o??=se(m,f),n.has(m)&&(o??=d("bundle contains a duplicate member",m,"each member path exactly once","Emit each archive member only once.",m)),n.add(m);const i=[];t.on("data",b=>i.push(b)),t.on("end",()=>{o===void 0&&r.push({path:m,type:f,bytes:new Uint8Array(Buffer.concat(i))})})},"onReadEntry")});l.on("ignoredEntry",t=>{const m=t.path;o??=d("bundle contains an ignored or unsupported archive member",`${m} (${t.type})`,"only supported regular file or directory members","Remove ignored or unsupported members from the archive.",m),t.resume()}),l.once("error",a),l.once("end",s);try{l.end(Buffer.from(e))}catch(t){a(t)}}).catch(s=>{o??=d("bundle archive could not be parsed",s instanceof Error?s.message:String(s),"a valid complete uncompressed tar archive","Fix the archive format and serve it again.")}),o===void 0?{members:r}:{issue:o}}p(ae,"parseArchive");function N(e){return typeof e=="number"&&Number.isInteger(e)&&e>=1}p(N,"isPositiveInt");function de(e){const r=p((t,m,f,i)=>({issue:d(t,m,f,"Fix the provider block in bundle.json.",`bundle.json.${i}`)}),"bad");if(typeof e!="object"||e===null||Array.isArray(e))return r("bundle.json provider must be an object",JSON.stringify(e),"{ id, major, groups }","provider");const n=e,o=Object.keys(n).filter(t=>!L.has(t));if(o.length>0)return r("bundle.json provider has unknown keys",o.join(", "),"only: id, major, groups","provider");if(typeof n.id!="string"||!O.test(n.id))return r("provider id must match [A-Za-z0-9_-]{1,64}",JSON.stringify(n.id),'a provider id of 1-64 letters, digits, "_" or "-"',"provider.id");if(!N(n.major))return r("provider major must be an integer of at least 1",JSON.stringify(n.major),"the served major as an integer >= 1","provider.major");const s=n.groups;if(!Array.isArray(s)||s.length===0)return r("provider groups must be a non-empty array",JSON.stringify(s),"at least one { group, consent_line, rate?, who, allowlist }","provider.groups");const a=[],l=new Set;for(let t=0;t<s.length;t++){const m=`provider.groups[${t}]`,f=s[t];if(typeof f!="object"||f===null||Array.isArray(f))return r("provider group must be an object",JSON.stringify(f),"{ group, consent_line, rate?, who, allowlist }",m);const i=f,b=Object.keys(i).filter(v=>!Y.has(v));if(b.length>0)return r("provider group has unknown keys",b.join(", "),"only: group, consent_line, rate, who, allowlist",m);if(typeof i.group!="string"||!O.test(i.group))return r("provider group id must match [A-Za-z0-9_-]{1,64}",JSON.stringify(i.group),'a group id of 1-64 letters, digits, "_" or "-"',`${m}.group`);if(l.has(i.group))return r("provider group ids must be unique",i.group,"each group declared once",`${m}.group`);if(l.add(i.group),typeof i.consent_line!="string"||i.consent_line.trim().length===0)return r("provider group consent_line must be a non-empty string",JSON.stringify(i.consent_line),"the consent line shown for this group",`${m}.consent_line`);const h=i.rate;if(h!=null&&(typeof h!="number"||!Number.isFinite(h)||h<0))return r("provider group rate must be a non-negative number or null",JSON.stringify(h),"a finite number >= 0, null, or omitted",`${m}.rate`);if(typeof i.who!="string"||!Q.has(i.who))return r("provider group who must be own|user|allowlist|any",JSON.stringify(i.who),"own | user | allowlist | any",`${m}.who`);const c=i.allowlist;if(!Array.isArray(c)||!c.every(v=>typeof v=="string"&&v.length>0))return r("provider group allowlist must be an array of non-empty strings",JSON.stringify(c),"an array of app principals ([] unless who is allowlist)",`${m}.allowlist`);if(c.length>0&&i.who!=="allowlist")return r("provider group allowlist must be empty unless who is allowlist",`who ${i.who}`,'who: "allowlist", or allowlist: []',`${m}.allowlist`);a.push({group:i.group,consent_line:i.consent_line,...h!==void 0?{rate:h}:{},who:i.who,allowlist:c})}return{provider:{id:n.id,major:n.major,groups:a}}}p(de,"parseProviderBlock");function ue(e){const r=[],n=p((o,s)=>{const a=[...s,o.name];if(o.kind==="leaf")r.push({path:a.join(" "),leaf:o});else for(const l of o.children)n(l,a)},"walk");for(const o of e.roots)n(o,[]);for(const o of e.coreMounts)n(o.node,o.parent);return r}p(ue,"collectLeaves");function le(e,r){const n=e.provider;if(n===void 0)return;if(e.transport===void 0)return d("a provider bundle must declare its transport","(missing)",'transport { kind: "http", endpoint } in bundle.json',"Add the provider endpoint as bundle.json transport.","bundle.json.transport");if(e.transport.kind==="http"&&e.transport.authEnv!==void 0)return d("a provider bundle must not declare authEnv",e.transport.authEnv,"no authEnv: the runtime presents its own outbound token","Remove transport.authEnv from bundle.json.","bundle.json.transport.authEnv");const o=new Set(n.groups.map(s=>s.group));for(const{path:s,leaf:a}of ue(r)){const l="group"in a?a.group:void 0;if(l===void 0)return d(`provider leaf "${s}" has no group`,s,"a group on every leaf of a provider bundle",`Add a group to leaf "${s}".`,`commands.json:${s}.group`);if(!o.has(l))return d(`provider leaf "${s}" names undeclared group "${l}"`,l,`one of: ${[...o].join(", ")}`,`Declare group "${l}" in the provider block, or move leaf "${s}" to a declared group.`,`commands.json:${s}.group`);if("rest"in a&&a.rest.streaming===!0)return d(`provider leaf "${s}" streams`,"rest.streaming: true","no streaming leaves in a provider bundle",`Make leaf "${s}" return one response.`,`commands.json:${s}.rest.streaming`)}}p(le,"providerBundleIssue");function me(e){let r;try{r=JSON.parse(Buffer.from(e).toString("utf8"))}catch{return{issue:d("bundle.json is not valid JSON","invalid JSON",'{"bundleVersion":1}',"Write valid bundle metadata.","bundle.json")}}if(typeof r!="object"||r===null||Array.isArray(r))return{issue:d("bundle.json must be a JSON object",JSON.stringify(r),"an object with bundleVersion set to 1 and optionally kinds",'Set bundle.json to {"bundleVersion":1}.',"bundle.json")};const n=r,o=Object.keys(n).filter(t=>!ee.has(t));if(n.bundleVersion!==1||o.length>0)return{issue:d("bundle.json must contain bundleVersion 1 and at most version, transport, provider, kinds, page_components, and memory_extensions",JSON.stringify(r),"an object with bundleVersion set to 1 and optional version, transport, provider, kinds, page_components, and memory_extensions",'Set bundle.json to {"bundleVersion":1} plus optional contribution blocks.',"bundle.json")};if(n.kinds!==void 0){const t=M(n.kinds);if(t.length>0)return{issue:d("bundle.json kinds block is invalid",t.join("; "),"each entry a full KindConfig (whenToUse required) or a sparse patch of launch fields","Fix the kinds entries in bundle.json.","bundle.json")}}if(n.page_components!==void 0){const t=K(n.page_components);if(t.length>0)return{issue:d("bundle.json page_components block is invalid",t.join("; "),"an array of page component registrations, each a kind string or {kind, description?, useWhen?, doc?, display?}","Fix the page_components entries in bundle.json.","bundle.json")}}let s;if(n.memory_extensions!==void 0)try{s=X(n.memory_extensions)}catch(t){return{issue:d("bundle.json memory_extensions block is invalid",t instanceof Error?t.message:String(t),"a valid memory_extensions declaration object","Fix the memory_extensions declarations in bundle.json.","bundle.json")}}if(n.version!==void 0&&!N(n.version))return{issue:d("bundle.json version must be an integer of at least 1",JSON.stringify(n.version),"the plugin major as an integer >= 1, or omitted","Fix version in bundle.json.","bundle.json.version")};let a;if(n.transport!==void 0){const t=Z(n.transport);if(t.transport===void 0)return{issue:d("bundle.json transport is invalid",t.errors.join("; "),'{ kind: "http", endpoint, authEnv? } or { kind: "machine" }',"Fix transport in bundle.json.","bundle.json.transport")};a=t.transport}let l;if(n.provider!==void 0){const t=de(n.provider);if(t.issue!==void 0)return{issue:t.issue};if(l=t.provider,n.version!==void 0&&n.version!==l.major)return{issue:d("bundle.json provider major must equal version",`version ${String(n.version)}, major ${l.major}`,"one major","Set provider.major and version to the same integer.","bundle.json.provider.major")}}return{...n.version!==void 0?{version:n.version}:{},...a!==void 0?{transport:a}:{},...l!==void 0?{provider:l}:{},...n.kinds!==void 0?{kinds:n.kinds}:{},...n.page_components!==void 0?{page_components:G(n.page_components)}:{},...s!==void 0?{memory_extensions:s}:{}}}p(me,"parseBundleMetadata");function pe(e,r,n,o=new Set){let s;try{s=JSON.parse(Buffer.from(e).toString("utf8"))}catch{return{issues:[d("commands.json is not valid JSON","invalid JSON","a valid command-manifest JSON object","Regenerate commands.json.","commands.json")]}}const a=B(s,{transport:"http",reservedCoreNames:r,extensibleCoreBranches:o,...n!==void 0?{coreCommandPaths:n}:{}});return{...a.manifest!==void 0?{commands:a.manifest}:{},issues:a.issues}}p(pe,"parseCommands");async function ce(e,r){const n=re(e);if(n!==void 0)return{issues:[n]};const o=await ae(e);if(o.issue!==void 0||o.members===void 0)return{issues:[o.issue]};const s=ie(o.members);if(s!==void 0)return{issues:[s]};const a=o.members.find(u=>u.path==="bundle.json"&&u.type==="file"),l=o.members.find(u=>u.path==="commands.json"&&u.type==="file");if(a===void 0||l===void 0)return{issues:[d("bundle must contain exactly one bundle.json and one commands.json regular file",`bundle.json=${a!==void 0}, commands.json=${l!==void 0}`,"one regular bundle.json and one regular commands.json member","Add the required metadata members to the archive.")]};const t=o.members.find(u=>u.path==="hooks.json"&&u.type==="file"),m=o.members.filter(u=>u.type==="file"&&u.path.startsWith("hooks/"));if((t!==void 0||o.members.some(u=>u.path==="hooks"||u.path.startsWith("hooks/")))&&(t===void 0||!m.some(u=>u.path==="hooks/dispatch")))return{issues:[d("bundle hook payload must contain both hooks.json and hooks/dispatch regular files",`hooks.json=${t!==void 0}, hooks/dispatch=${m.some(u=>u.path==="hooks/dispatch")}`,"both hooks.json and hooks/dispatch regular files","Add both required hook members or remove the hook payload.")]};const i=me(a.bytes);if(i.issue!==void 0)return{issues:[i.issue]};const b=pe(l.bytes,r.reservedCoreNames,r.coreCommandPaths,r.extensibleCoreBranches);if(b.commands===void 0)return{issues:b.issues};const h=b.commands.version;if(i.version!==void 0&&h!==void 0&&i.version!==h)return{issues:[d("bundle.json version and commands.json version differ",`bundle.json ${i.version}, commands.json ${h}`,"one plugin major","Emit the same version in bundle.json and commands.json.","bundle.json.version")]};const c=i.version??h;if(i.provider!==void 0&&c===void 0)return{issues:[d("a provider bundle requires a plugin version","no version in bundle.json or commands.json",`version ${i.provider.major}, the provider major`,"Emit the provider major as the plugin version.","bundle.json.version")]};if(i.provider!==void 0&&c!==i.provider.major)return{issues:[d("bundle provider major must equal the plugin version",`version ${c}, major ${i.provider.major}`,"one major","Emit the provider major as the plugin version.","bundle.json.provider.major")]};const v=le(i,b.commands);return v!==void 0?{issues:[v]}:{bundle:{bundleVersion:1,...c!==void 0?{version:c}:{},...i.transport!==void 0?{transport:i.transport}:{},...i.provider!==void 0?{provider:i.provider}:{},...i.kinds!==void 0?{kinds:i.kinds}:{},...i.page_components!==void 0?{page_components:i.page_components}:{},...i.memory_extensions!==void 0?{memory_extensions:i.memory_extensions}:{},commands:b.commands,commandsBytes:l.bytes,directories:o.members.filter(u=>u.type==="directory").map(u=>({path:u.path})),memory:o.members.filter(u=>u.type==="file"&&u.path.startsWith("memory/")).map(u=>({path:u.path,bytes:u.bytes})),...t!==void 0?{hooks:{manifest:t.bytes,files:m.map(u=>({path:u.path,bytes:u.bytes}))}}:{}},issues:[]}}p(ce,"validatePluginBundle");function fe(e){return I("sha256").update(e).digest("hex").slice(0,12)}p(fe,"bundleVersion");function Ne(e,r,n,o){return R(r).plugins[e]?.version!==o?!1:_(w(n,"commands.json"))}p(Ne,"isInstalledGeneration");function he(e,r,n,o){C(e,s=>{s.plugins[r]===void 0&&(s.plugins[r]={}),o.swapped&&(s.plugins[r].last_updated=E()),s.plugins[r].checked_at=E(),n===void 0?delete s.plugins[r].etag:s.plugins[r].etag=n})}p(he,"recordCheckedGeneration");function g(e,r){const n=V(e,r);if(n===e||!n.startsWith(e+z))throw x(`bundle member path escapes the staging directory: ${r}`);return n}p(g,"stagedPath");function be(e,r,n){const o=p((s,a,l=420)=>q(s,a,{mode:l,durable:!0}),"write");T(g(e,".crouter-plugin/plugin.json"),r,{durable:!0});for(const s of n.directories)j(g(e,s.path));o(g(e,"commands.json"),n.commandsBytes);for(const s of n.memory)o(g(e,s.path),s.bytes);if(n.hooks!==void 0){o(g(e,"hooks.json"),n.hooks.manifest);for(const s of n.hooks.files)o(g(e,s.path),s.bytes);W(g(e,"hooks/dispatch"),493)}H(e)}p(be,"writeStagedBundle");async function Ae(e,r,n){const{scope:o,scopeRootPath:s,root:a}=n,l=await ce(r,n.validation);if(l.bundle===void 0)throw n.invalid(`HTTP plugin bundle for "${e}" is invalid`,l.issues);const t=l.bundle,m=n.accept?.(t);if(m!==void 0)throw n.invalid(`HTTP plugin bundle for "${e}" is invalid`,[d(m,e,"the bundle this install asked for","Serve the requested bundle.")]);const f=n.version??fe(r),i=t.provider!==void 0&&t.transport!==void 0?t.transport:n.transport,b={name:e,version:f,commands:"commands.json",transport:i,bundle:n.bundle,...t.kinds!==void 0?{kinds:t.kinds}:{},...t.page_components!==void 0?{page_components:t.page_components}:{},...t.memory_extensions!==void 0?{memory_extensions:t.memory_extensions}:{},...t.hooks!==void 0?{hooks:"hooks.json",hookExecutable:"hooks/dispatch"}:{},...t.provider!==void 0?{provider:t.provider}:{}},h=w(s,"tmp"),c=w(h,`${e}.${process.pid}`),v=`${c}.prev`;if(_(v))throw x(`cannot replace plugin "${e}": prior interrupted replacement remains at ${v}`);j(h),j($(a)),S(c);let u;try{j(c),be(c,b,t),u=await n.validateStaged(c,b);let y=!1;_(a)&&(k(a,v),y=!0);try{k(c,a)}catch(A){throw y&&k(v,a),A}U($(a)),y&&S(v)}finally{S(c)}return F(o,s),D(o,y=>{y.plugins[e]={enabled:n.enable?!0:y.plugins[e]?.enabled??!0,version:f}}),he(o,e,n.etag,{swapped:!0}),{manifest:b,validated:t,version:f,root:a,staged:u}}p(Ae,"installBundleArchive");export{fe as bundleVersion,Ae as installBundleArchive,Ne as isInstalledGeneration,he as recordCheckedGeneration,ce as validatePluginBundle,be as writeStagedBundle};
|
|
1
|
+
var P=Object.defineProperty;var c=(e,o)=>P(e,"name",{value:o,configurable:!0});import{Parser as J}from"tar";import{createHash as I}from"node:crypto";import{chmodSync as W,renameSync as k}from"node:fs";import{dirname as $,join as w,resolve as B,sep as V}from"node:path";import{validateCommandManifest as z}from"../../api/command-manifest/manifest.js";import{ensureScopeInitialized as R,invalidPluginKindsReasons as F,readConfig as M,updateConfig as D,updateState as C}from"../config.js";import{general as x}from"../errors.js";import{atomicWriteJson as T,atomicWriteText as q,ensureDir as j,nowIso as E,pathExists as _,removePath as S,syncDirectory as U,syncTree as H}from"../fs-utils.js";import{invalidPageComponentsReasons as K,normalizePageComponents as G}from"../human/page-catalog.js";import{validateMemoryExtensionDeclarations as X}from"../plugin-extensions.js";import{objectBlocksShapeReasons as Z}from"../plugin-objects-manifest.js";import{validateBundleTransport as L}from"./endpoint.js";const O=/^[A-Za-z0-9_-]{1,64}$/,Y=new Set(["id","major","groups"]),Q=new Set(["group","consent_line","rate","who","allowlist"]),ee=new Set(["own","user","allowlist","any"]),ne=new Set(["bundleVersion","kinds","page_components","memory_extensions","version","transport","provider","object_kinds","scopes"]);function oe(e,o,n,r,s,a){return{code:e,message:o,received:n,expected:r,next:s,...a!==void 0?{path:a}:{}}}c(oe,"issue");function u(e,o,n,r,s){return oe("plugin_bundle_invalid",e,o,n,r,s)}c(u,"bundleInvalid");function re(e){if(e[0]===31&&e[1]===139)return u("bundle archive must be uncompressed tar data","gzip-compressed archive","an uncompressed tar archive","Serve the archive without compression.");if(e.length===0||e.length%512!==0)return u("bundle archive must be a non-empty whole number of 512-byte tar blocks",`${e.length} bytes`,"a non-empty tar stream with a 512-byte block length","Serve one complete uncompressed tar archive.");if(e.length<1024||!e.subarray(-1024).every(o=>o===0))return u("bundle archive is missing the required two zero-block terminator","archive tail is not 1024 zero bytes","a complete tar stream ending in two 512-byte zero blocks","Serve the complete uncompressed tar archive.")}c(re,"archiveEnvelopeIssue");function se(e,o){if(e.length===0||e.includes("\0")||e.includes("\\")||e.startsWith("/")||/^[A-Za-z]:($|\/)/.test(e))return u("bundle member path must be a safe relative POSIX path",e,"a relative path without absolute, drive-letter, backslash, or NUL forms","Use a safe relative POSIX member path.",e);if(o==="file"&&e.endsWith("/"))return u("only directory members may have a trailing slash",e,"a regular-file path without a trailing slash","Emit this member as a regular file path.",e);if((o==="directory"&&e.endsWith("/")?e.slice(0,-1):e).split("/").some(r=>r===""||r==="."||r===".."))return u("bundle member path contains an unsafe segment",e,"non-empty segments other than . or ..","Use a safe relative POSIX member path.",e)}c(se,"validateMemberName");function te(e,o){if(o==="file")return e==="bundle.json"||e==="commands.json"||e==="hooks.json"||e.startsWith("memory/")&&e.endsWith(".md")||e.startsWith("hooks/")?void 0:u("bundle contains an unsupported file member",e,"bundle.json, commands.json, hooks.json, memory/**/*.md, or hooks/**","Remove unsupported members from the archive.",e);if(!(e==="memory"||e.startsWith("memory/")||e==="hooks"||e.startsWith("hooks/")))return u("bundle contains an unsupported directory member",e,"memory or hooks, or a directory below either subtree","Remove unsupported directory members from the archive.",e)}c(te,"validateMemberShape");function ie(e){return e.type==="directory"&&e.path.endsWith("/")?e.path.slice(0,-1):e.path}c(ie,"materializedPath");function de(e){const o=new Map;for(const n of e){const r=ie(n),s=o.get(r);if(s!==void 0)return u(s===n.type?"bundle contains a duplicate member":"bundle claims one path as both a file and a directory",`${r} (${s} and ${n.type})`,"each materialized path exactly once, with one type","Emit each archive path once, as a single member type.",r);o.set(r,n.type)}for(const[n,r]of o)if(r==="file"){for(const s of o.keys())if(s.startsWith(`${n}/`))return u("bundle nests a member beneath a regular-file path",`${s} beneath file ${n}`,"members nested only beneath directory paths","Emit the parent path as a directory or remove the nested member.",s)}}c(de,"validateMemberTree");async function ae(e){const o=[],n=new Set;let r;return await new Promise((s,a)=>{const p=new J({strict:!0,maxMetaEntrySize:Number.MAX_SAFE_INTEGER,gzip:!1,brotli:!1,zstd:!1,onReadEntry:c(t=>{const d=t.path,f=t.type==="File"||t.type==="OldFile"?"file":t.type==="Directory"?"directory":void 0;if(f===void 0||t.unsupported||t.ignore||t.linkpath!==void 0){r??=u("bundle member must be a regular file or directory without a link target",`${d} (${t.type})`,"a regular file or directory member without link metadata","Remove links and unsupported member types from the archive.",d),t.resume();return}r??=se(d,f),r??=te(d,f),n.has(d)&&(r??=u("bundle contains a duplicate member",d,"each member path exactly once","Emit each archive member only once.",d)),n.add(d);const i=[];t.on("data",b=>i.push(b)),t.on("end",()=>{r===void 0&&o.push({path:d,type:f,bytes:new Uint8Array(Buffer.concat(i))})})},"onReadEntry")});p.on("ignoredEntry",t=>{const d=t.path;r??=u("bundle contains an ignored or unsupported archive member",`${d} (${t.type})`,"only supported regular file or directory members","Remove ignored or unsupported members from the archive.",d),t.resume()}),p.once("error",a),p.once("end",s);try{p.end(Buffer.from(e))}catch(t){a(t)}}).catch(s=>{r??=u("bundle archive could not be parsed",s instanceof Error?s.message:String(s),"a valid complete uncompressed tar archive","Fix the archive format and serve it again.")}),r===void 0?{members:o}:{issue:r}}c(ae,"parseArchive");function N(e){return typeof e=="number"&&Number.isInteger(e)&&e>=1}c(N,"isPositiveInt");function ue(e){const o=c((t,d,f,i)=>({issue:u(t,d,f,"Fix the provider block in bundle.json.",`bundle.json.${i}`)}),"bad");if(typeof e!="object"||e===null||Array.isArray(e))return o("bundle.json provider must be an object",JSON.stringify(e),"{ id, major, groups }","provider");const n=e,r=Object.keys(n).filter(t=>!Y.has(t));if(r.length>0)return o("bundle.json provider has unknown keys",r.join(", "),"only: id, major, groups","provider");if(typeof n.id!="string"||!O.test(n.id))return o("provider id must match [A-Za-z0-9_-]{1,64}",JSON.stringify(n.id),'a provider id of 1-64 letters, digits, "_" or "-"',"provider.id");if(!N(n.major))return o("provider major must be an integer of at least 1",JSON.stringify(n.major),"the served major as an integer >= 1","provider.major");const s=n.groups;if(!Array.isArray(s)||s.length===0)return o("provider groups must be a non-empty array",JSON.stringify(s),"at least one { group, consent_line, rate?, who, allowlist }","provider.groups");const a=[],p=new Set;for(let t=0;t<s.length;t++){const d=`provider.groups[${t}]`,f=s[t];if(typeof f!="object"||f===null||Array.isArray(f))return o("provider group must be an object",JSON.stringify(f),"{ group, consent_line, rate?, who, allowlist }",d);const i=f,b=Object.keys(i).filter(v=>!Q.has(v));if(b.length>0)return o("provider group has unknown keys",b.join(", "),"only: group, consent_line, rate, who, allowlist",d);if(typeof i.group!="string"||!O.test(i.group))return o("provider group id must match [A-Za-z0-9_-]{1,64}",JSON.stringify(i.group),'a group id of 1-64 letters, digits, "_" or "-"',`${d}.group`);if(p.has(i.group))return o("provider group ids must be unique",i.group,"each group declared once",`${d}.group`);if(p.add(i.group),typeof i.consent_line!="string"||i.consent_line.trim().length===0)return o("provider group consent_line must be a non-empty string",JSON.stringify(i.consent_line),"the consent line shown for this group",`${d}.consent_line`);const h=i.rate;if(h!=null&&(typeof h!="number"||!Number.isFinite(h)||h<0))return o("provider group rate must be a non-negative number or null",JSON.stringify(h),"a finite number >= 0, null, or omitted",`${d}.rate`);if(typeof i.who!="string"||!ee.has(i.who))return o("provider group who must be own|user|allowlist|any",JSON.stringify(i.who),"own | user | allowlist | any",`${d}.who`);const m=i.allowlist;if(!Array.isArray(m)||!m.every(v=>typeof v=="string"&&v.length>0))return o("provider group allowlist must be an array of non-empty strings",JSON.stringify(m),"an array of app principals ([] unless who is allowlist)",`${d}.allowlist`);if(m.length>0&&i.who!=="allowlist")return o("provider group allowlist must be empty unless who is allowlist",`who ${i.who}`,'who: "allowlist", or allowlist: []',`${d}.allowlist`);a.push({group:i.group,consent_line:i.consent_line,...h!==void 0?{rate:h}:{},who:i.who,allowlist:m})}return{provider:{id:n.id,major:n.major,groups:a}}}c(ue,"parseProviderBlock");function le(e){const o=[],n=c((r,s)=>{const a=[...s,r.name];if(r.kind==="leaf")o.push({path:a.join(" "),leaf:r});else for(const p of r.children)n(p,a)},"walk");for(const r of e.roots)n(r,[]);for(const r of e.coreMounts)n(r.node,r.parent);return o}c(le,"collectLeaves");function pe(e,o){const n=e.provider;if(n===void 0)return;if(e.transport===void 0)return u("a provider bundle must declare its transport","(missing)",'transport { kind: "http", endpoint } in bundle.json',"Add the provider endpoint as bundle.json transport.","bundle.json.transport");if(e.transport.kind==="http"&&e.transport.authEnv!==void 0)return u("a provider bundle must not declare authEnv",e.transport.authEnv,"no authEnv: the runtime presents its own outbound token","Remove transport.authEnv from bundle.json.","bundle.json.transport.authEnv");const r=new Set(n.groups.map(s=>s.group));for(const{path:s,leaf:a}of le(o)){const p="group"in a?a.group:void 0;if(p===void 0)return u(`provider leaf "${s}" has no group`,s,"a group on every leaf of a provider bundle",`Add a group to leaf "${s}".`,`commands.json:${s}.group`);if(!r.has(p))return u(`provider leaf "${s}" names undeclared group "${p}"`,p,`one of: ${[...r].join(", ")}`,`Declare group "${p}" in the provider block, or move leaf "${s}" to a declared group.`,`commands.json:${s}.group`);if("rest"in a&&a.rest.streaming===!0)return u(`provider leaf "${s}" streams`,"rest.streaming: true","no streaming leaves in a provider bundle",`Make leaf "${s}" return one response.`,`commands.json:${s}.rest.streaming`)}}c(pe,"providerBundleIssue");function ce(e){let o;try{o=JSON.parse(Buffer.from(e).toString("utf8"))}catch{return{issue:u("bundle.json is not valid JSON","invalid JSON",'{"bundleVersion":1}',"Write valid bundle metadata.","bundle.json")}}if(typeof o!="object"||o===null||Array.isArray(o))return{issue:u("bundle.json must be a JSON object",JSON.stringify(o),"an object with bundleVersion set to 1 and optionally kinds",'Set bundle.json to {"bundleVersion":1}.',"bundle.json")};const n=o,r=Object.keys(n).filter(d=>!ne.has(d));if(n.bundleVersion!==1||r.length>0)return{issue:u("bundle.json must contain bundleVersion 1 and at most version, transport, provider, kinds, page_components, memory_extensions, object_kinds, and scopes",JSON.stringify(o),"an object with bundleVersion set to 1 and optional version, transport, provider, kinds, page_components, memory_extensions, object_kinds, and scopes",'Set bundle.json to {"bundleVersion":1} plus optional contribution blocks.',"bundle.json")};if(n.kinds!==void 0){const d=F(n.kinds);if(d.length>0)return{issue:u("bundle.json kinds block is invalid",d.join("; "),"each entry a full KindConfig (whenToUse required) or a sparse patch of launch fields","Fix the kinds entries in bundle.json.","bundle.json")}}if(n.page_components!==void 0){const d=K(n.page_components);if(d.length>0)return{issue:u("bundle.json page_components block is invalid",d.join("; "),"an array of page component registrations, each a kind string or {kind, description?, useWhen?, doc?, display?}","Fix the page_components entries in bundle.json.","bundle.json")}}const s=Z(n);if(n.provider!==void 0&&n.scopes!==void 0&&s.push("scopes is for local plugins; a provider gets its scopes from directory consent"),s.length>0)return{issue:u("bundle.json object_kinds or scopes block is invalid",s.join("; "),"object_kinds keyed by kind name with description, events and optional read.command; scopes an array of crtr:objects:<verb>:<target> strings (local plugins only)","Fix the object_kinds and scopes blocks in bundle.json.","bundle.json")};let a;if(n.memory_extensions!==void 0)try{a=X(n.memory_extensions)}catch(d){return{issue:u("bundle.json memory_extensions block is invalid",d instanceof Error?d.message:String(d),"a valid memory_extensions declaration object","Fix the memory_extensions declarations in bundle.json.","bundle.json")}}if(n.version!==void 0&&!N(n.version))return{issue:u("bundle.json version must be an integer of at least 1",JSON.stringify(n.version),"the plugin major as an integer >= 1, or omitted","Fix version in bundle.json.","bundle.json.version")};let p;if(n.transport!==void 0){const d=L(n.transport);if(d.transport===void 0)return{issue:u("bundle.json transport is invalid",d.errors.join("; "),'{ kind: "http", endpoint, authEnv? } or { kind: "machine" }',"Fix transport in bundle.json.","bundle.json.transport")};p=d.transport}let t;if(n.provider!==void 0){const d=ue(n.provider);if(d.issue!==void 0)return{issue:d.issue};if(t=d.provider,n.version!==void 0&&n.version!==t.major)return{issue:u("bundle.json provider major must equal version",`version ${String(n.version)}, major ${t.major}`,"one major","Set provider.major and version to the same integer.","bundle.json.provider.major")}}return{...n.version!==void 0?{version:n.version}:{},...p!==void 0?{transport:p}:{},...t!==void 0?{provider:t}:{},...n.kinds!==void 0?{kinds:n.kinds}:{},...n.page_components!==void 0?{page_components:G(n.page_components)}:{},...a!==void 0?{memory_extensions:a}:{},...n.object_kinds!==void 0?{object_kinds:n.object_kinds}:{},...n.scopes!==void 0?{scopes:n.scopes}:{}}}c(ce,"parseBundleMetadata");function me(e,o,n,r=new Set){let s;try{s=JSON.parse(Buffer.from(e).toString("utf8"))}catch{return{issues:[u("commands.json is not valid JSON","invalid JSON","a valid command-manifest JSON object","Regenerate commands.json.","commands.json")]}}const a=z(s,{transport:"http",reservedCoreNames:o,extensibleCoreBranches:r,...n!==void 0?{coreCommandPaths:n}:{}});return{...a.manifest!==void 0?{commands:a.manifest}:{},issues:a.issues}}c(me,"parseCommands");async function fe(e,o){const n=re(e);if(n!==void 0)return{issues:[n]};const r=await ae(e);if(r.issue!==void 0||r.members===void 0)return{issues:[r.issue]};const s=de(r.members);if(s!==void 0)return{issues:[s]};const a=r.members.find(l=>l.path==="bundle.json"&&l.type==="file"),p=r.members.find(l=>l.path==="commands.json"&&l.type==="file");if(a===void 0||p===void 0)return{issues:[u("bundle must contain exactly one bundle.json and one commands.json regular file",`bundle.json=${a!==void 0}, commands.json=${p!==void 0}`,"one regular bundle.json and one regular commands.json member","Add the required metadata members to the archive.")]};const t=r.members.find(l=>l.path==="hooks.json"&&l.type==="file"),d=r.members.filter(l=>l.type==="file"&&l.path.startsWith("hooks/"));if((t!==void 0||r.members.some(l=>l.path==="hooks"||l.path.startsWith("hooks/")))&&(t===void 0||!d.some(l=>l.path==="hooks/dispatch")))return{issues:[u("bundle hook payload must contain both hooks.json and hooks/dispatch regular files",`hooks.json=${t!==void 0}, hooks/dispatch=${d.some(l=>l.path==="hooks/dispatch")}`,"both hooks.json and hooks/dispatch regular files","Add both required hook members or remove the hook payload.")]};const i=ce(a.bytes);if(i.issue!==void 0)return{issues:[i.issue]};const b=me(p.bytes,o.reservedCoreNames,o.coreCommandPaths,o.extensibleCoreBranches);if(b.commands===void 0)return{issues:b.issues};const h=b.commands.version;if(i.version!==void 0&&h!==void 0&&i.version!==h)return{issues:[u("bundle.json version and commands.json version differ",`bundle.json ${i.version}, commands.json ${h}`,"one plugin major","Emit the same version in bundle.json and commands.json.","bundle.json.version")]};const m=i.version??h;if(i.provider!==void 0&&m===void 0)return{issues:[u("a provider bundle requires a plugin version","no version in bundle.json or commands.json",`version ${i.provider.major}, the provider major`,"Emit the provider major as the plugin version.","bundle.json.version")]};if(i.provider!==void 0&&m!==i.provider.major)return{issues:[u("bundle provider major must equal the plugin version",`version ${m}, major ${i.provider.major}`,"one major","Emit the provider major as the plugin version.","bundle.json.provider.major")]};const v=pe(i,b.commands);return v!==void 0?{issues:[v]}:{bundle:{bundleVersion:1,...m!==void 0?{version:m}:{},...i.transport!==void 0?{transport:i.transport}:{},...i.provider!==void 0?{provider:i.provider}:{},...i.kinds!==void 0?{kinds:i.kinds}:{},...i.page_components!==void 0?{page_components:i.page_components}:{},...i.memory_extensions!==void 0?{memory_extensions:i.memory_extensions}:{},...i.object_kinds!==void 0?{object_kinds:i.object_kinds}:{},...i.scopes!==void 0?{scopes:i.scopes}:{},commands:b.commands,commandsBytes:p.bytes,directories:r.members.filter(l=>l.type==="directory").map(l=>({path:l.path})),memory:r.members.filter(l=>l.type==="file"&&l.path.startsWith("memory/")).map(l=>({path:l.path,bytes:l.bytes})),...t!==void 0?{hooks:{manifest:t.bytes,files:d.map(l=>({path:l.path,bytes:l.bytes}))}}:{}},issues:[]}}c(fe,"validatePluginBundle");function he(e){return I("sha256").update(e).digest("hex").slice(0,12)}c(he,"bundleVersion");function Pe(e,o,n,r){return M(o).plugins[e]?.version!==r?!1:_(w(n,"commands.json"))}c(Pe,"isInstalledGeneration");function be(e,o,n,r){C(e,s=>{s.plugins[o]===void 0&&(s.plugins[o]={}),r.swapped&&(s.plugins[o].last_updated=E()),s.plugins[o].checked_at=E(),n===void 0?delete s.plugins[o].etag:s.plugins[o].etag=n})}c(be,"recordCheckedGeneration");function g(e,o){const n=B(e,o);if(n===e||!n.startsWith(e+V))throw x(`bundle member path escapes the staging directory: ${o}`);return n}c(g,"stagedPath");function ve(e,o,n){const r=c((s,a,p=420)=>q(s,a,{mode:p,durable:!0}),"write");T(g(e,".crouter-plugin/plugin.json"),o,{durable:!0});for(const s of n.directories)j(g(e,s.path));r(g(e,"commands.json"),n.commandsBytes);for(const s of n.memory)r(g(e,s.path),s.bytes);if(n.hooks!==void 0){r(g(e,"hooks.json"),n.hooks.manifest);for(const s of n.hooks.files)r(g(e,s.path),s.bytes);W(g(e,"hooks/dispatch"),493)}H(e)}c(ve,"writeStagedBundle");async function Je(e,o,n){const{scope:r,scopeRootPath:s,root:a}=n,p=await fe(o,n.validation);if(p.bundle===void 0)throw n.invalid(`HTTP plugin bundle for "${e}" is invalid`,p.issues);const t=p.bundle,d=n.accept?.(t);if(d!==void 0)throw n.invalid(`HTTP plugin bundle for "${e}" is invalid`,[u(d,e,"the bundle this install asked for","Serve the requested bundle.")]);const f=n.version??he(o),i=t.provider!==void 0&&t.transport!==void 0?t.transport:n.transport,b={name:e,version:f,commands:"commands.json",transport:i,bundle:n.bundle,...t.kinds!==void 0?{kinds:t.kinds}:{},...t.page_components!==void 0?{page_components:t.page_components}:{},...t.memory_extensions!==void 0?{memory_extensions:t.memory_extensions}:{},...t.object_kinds!==void 0?{object_kinds:t.object_kinds}:{},...t.scopes!==void 0?{scopes:t.scopes}:{},...t.hooks!==void 0?{hooks:"hooks.json",hookExecutable:"hooks/dispatch"}:{},...t.provider!==void 0?{provider:t.provider}:{}},h=w(s,"tmp"),m=w(h,`${e}.${process.pid}`),v=`${m}.prev`;if(_(v))throw x(`cannot replace plugin "${e}": prior interrupted replacement remains at ${v}`);j(h),j($(a)),S(m);let l;try{j(m),ve(m,b,t),l=await n.validateStaged(m,b);let y=!1;_(a)&&(k(a,v),y=!0);try{k(m,a)}catch(A){throw y&&k(v,a),A}U($(a)),y&&S(v)}finally{S(m)}return R(r,s),D(r,y=>{y.plugins[e]={enabled:n.enable?!0:y.plugins[e]?.enabled??!0,version:f}}),be(r,e,n.etag,{swapped:!0}),{manifest:b,validated:t,version:f,root:a,staged:l}}c(Je,"installBundleArchive");export{he as bundleVersion,Je as installBundleArchive,Pe as isInstalledGeneration,be as recordCheckedGeneration,fe as validatePluginBundle,ve as writeStagedBundle};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CrtrCallerFields {
|
|
2
|
+
node?: string | undefined;
|
|
3
|
+
machine?: string | undefined;
|
|
4
|
+
profile?: string | undefined;
|
|
5
|
+
run?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** `node=<id>; machine=<id>; profile=<id>; run=<id>`, in that order. A field
|
|
8
|
+
* that is empty, or whose value holds `;`, `=`, CR or LF, is left out rather
|
|
9
|
+
* than injected into the header. Returns '' when no field remains. */
|
|
10
|
+
export declare function formatCrtrCaller(fields: CrtrCallerFields): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var f=Object.defineProperty;var e=(o,n)=>f(o,"name",{value:n,configurable:!0});function s(o){const n=[];for(const r of["node","machine","profile","run"]){const t=o[r];typeof t!="string"||t===""||/[;=\r\n]/.test(t)||n.push(`${r}=${t}`)}return n.join("; ")}e(s,"formatCrtrCaller");export{s as formatCrtrCaller};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var x=Object.defineProperty;var o=(n,e)=>x(n,"name",{value:e,configurable:!0});import{resolve as T}from"node:path";import{envNodeId as j}from"../../shared/env.js";import{cliClient as m}from"../../commands/api-client.js";import{apiErrorToCliError as l}from"../io.js";import{ApiError as f}from"../../api/index.js";import{defineBranch as v,defineLeaf as A}from"../command.js";import{resolveCommandRegistry as _}from"../command-manifests/registry.js";import{executeExternalLeaf as L}from"./transport/exec-invoke.js";import{invokeHttpLeaf as M}from"./transport/http-invoke.js";import{readPathParam as U}from"./transport/http-plan.js";import{sha256Hex as N,signedHeaderDigest as P}from"./transport/sign.js";import{needsAppToken as S}from"../plugin-objects-manifest.js";async function R(n){const e=await m().request("GET",`/v1/providers/${encodeURIComponent(n)}/tools`);return new Set(e.groups.flatMap(r=>r.leaves.map(a=>a.tool)))}o(R,"allowedProviderTools");function h(n,e,r){return n.children.some(a=>a.kind==="leaf"?r.has([...e,a.name].join(" ")):h(a,[...e,a.name],r))}o(h,"hasAllowedLeaf");function H(n,e,r){return n.children.filter(a=>a.tier!=="hidden"&&(a.kind==="leaf"?r.has([...e,a.name].join(" ")):h(a,[...e,a.name],r))).length}o(H,"visibleChildCount");function $(n,e,r){return async()=>{if(!j())return n.help;const a=await R(e.id);return{...n.help,listing:n.help.listing?.flatMap(t=>{const i=n.children.find(s=>s.name===t.name);if(i?.kind==="leaf")return a.has([...r,i.name].join(" "))?[t]:[];if(i?.kind!=="branch")return[];const u=[...r,i.name];if(!h(i,u,a))return[];const p=H(i,u,a);return[{...t,...t.subCount!==void 0?{subCount:p}:{}}]})}}}o($,"providerHelp");const I="Runs on one of your devices. Add `--machine <machine_id>` to pick which; without it the call goes to your only device, and with several the answer is `machine_required` listing each one's machine_id. The `runtime:` prefix on the id is optional.";function y(n,e,r,a=!1){return{outputKind:"object",...a?{machineSelector:!0}:{},run:o(async(t,i)=>{const u={...t};for(const p of r.params){if(!("type"in p))continue;const s=p.name.replace(/-([a-z])/g,(c,C)=>C.toUpperCase()),d=u[s];p.type==="file"&&typeof d=="string"?u[s]=T(d):p.type==="path"&&d!==void 0&&(u[s]=U(`provider command \`${n} ${e}\``,p,d))}try{return await m({timeoutMs:0}).request("POST","/v1/provider/call",{provider:n,tool:e,arguments:u,...i?.machine===void 0?{}:{machine:i.machine}},{timeout:0})}catch(p){throw p instanceof f?l(p,void 0,{providerCall:!0}):p}},"run")}}o(y,"providerLeafAdapter");function E(n,e){return/^[A-Za-z0-9_-]{1,64}$/.test(n)&&n!=="crtr"&&!e.has(n)}o(E,"validRootName");function Y(n,e){const r=[],a=[];for(const t of n){const{provider:i,commands:u}=t;if(!Array.isArray(u)||t.status!=="available"&&!E(i,e))continue;if(t.status!=="available"){const s=t.error??"bundle installation pending",d=`Unavailable (${t.status}): ${s}`,c=v({name:i,rootEntry:{concept:t.display_name,desc:d,useWhen:d},help:{name:i,summary:d},children:[]});c.unavailable=d,a.push(c);continue}const p=o(s=>s.kind==="branch"?{...s,children:s.children.map(p)}:{...s,rest:{method:"GET",path:"/",params:{}}},"helpNode");for(const s of u)s.kind!=="branch"||!Array.isArray(s.children)||!E(s.name,e)||r.push({contributor:{name:i,scope:"user"},manifestName:s.name,node:p(s),adaptLeaf:o((d,c)=>y(i,c.slice(1).join(" "),d,t.transport==="machine"),"adaptLeaf")})}return[...b(_(r,e)),...a]}o(Y,"composeSandboxProviderSubtrees");async function K(n){try{const{token:e,socket:r}=await m().plugins.appToken(n);return{CRTR_APP_TOKEN:e,CRTR_DAEMON_SOCKET:r}}catch(e){if(e instanceof f&&(e.status===404||e.status===403))return;throw e instanceof f?l(e):e}}o(K,"appEnvFor");function w(n,e){return(r,a)=>{const t=n.transport,i=e(a);if(n.provider!==void 0)return y(n.provider.id,i.slice(1).join(" "),r,t.kind==="machine");if(t.kind==="exec"){const p=t.executable;if(p===void 0)throw new Error(`exec plugin "${n.plugin.name}" has an executable leaf without transport.executable`);return{outputKind:"object",run:o(async s=>{const d=S(n.plugin.manifest)?await K(n.plugin.name):void 0;return L({plugin:n.plugin,executable:p,commandPath:i,output:r.output,...d!==void 0?{appEnv:d}:{}},s)},"run")}}if(t.kind==="machine")throw new Error(`machine plugin "${n.plugin.name}" is not a capability provider`);const u=r;return{outputKind:u.rest.streaming===!0?"jsonl":"object",run:o((p,s)=>M({registration:{name:n.plugin.name,endpoint:t.endpoint,...t.authEnv!==void 0?{authEnv:t.authEnv}:{}},...n.manifest?.baseUrl!==void 0?{baseUrl:n.manifest.baseUrl}:{},...n.manifest?.timeouts!==void 0?{timeouts:n.manifest.timeouts}:{},params:u.params,rest:u.rest,output:u.output,commandPath:i},p,s,{callHeaders:o(d=>m().plugins.callHeaders(n.plugin.name,{method:d.method,url:d.url.href,body_sha256:N(d.body),...P(d.headers)}).then(c=>c.headers,c=>{throw c instanceof f?l(c):c}),"callHeaders")}),"run")}}}o(w,"adaptLeaf");function ee(n){return{contributions:n.map(e=>({contributor:{name:e.plugin.name,scope:e.plugin.scope,sourceMarketplace:e.plugin.sourceMarketplace},node:e.node,manifestName:e.manifestName,adaptLeaf:w(e,r=>[e.manifestName,...r.slice(1)]),...e.provider!==void 0?{provider:e.provider}:{}})),coreMounts:[],issues:[]}}o(ee,"adaptPluginContributions");function ne(n){return n.map(e=>({parent:e.parent,plugin:e.plugin.name,contributor:{name:e.plugin.name,scope:e.plugin.scope,sourceMarketplace:e.plugin.sourceMarketplace},node:e.node,...e.provider!==void 0?{provider:e.provider}:{},adaptLeaf:w({...e,manifestName:e.node.name},r=>e.provider===void 0?[...r]:[e.provider.id,...r.slice(e.parent.length)])}))}o(ne,"adaptPluginCoreMounts");function b(n){return n.contributions.map(e=>g(e,e.node,[e.node.name]))}o(b,"composeExternalSubtrees");function re(n,e){return n.filter(r=>r.parent.join("\0")===e.join("\0")).map(r=>r.node.kind==="branch"?g(r,r.node,[...e,r.node.name],r.provider===void 0?1:e.length):k(r,r.node,[...e,r.node.name]))}o(re,"composeCoreBranchMounts");function g(n,e,r,a=1){const t=e.rootEntry===void 0?void 0:{concept:e.rootEntry.concept,desc:e.rootEntry.description,useWhen:e.rootEntry.whenToUse},i=v({name:e.name,description:e.description,whenToUse:e.whenToUse,...e.tier!==void 0?{tier:e.tier}:{},...n.plugin!==void 0?{plugin:n.plugin}:{},...t!==void 0?{rootEntry:t}:{},help:{name:r.join(" "),summary:e.summary,...e.model!==void 0?{model:e.model}:{},...r.length===1&&n.extensionIssue!==void 0?{extensionIssue:n.extensionIssue}:{}},...e.passthrough!==void 0?{passthrough:e.passthrough}:{},children:e.children.map(u=>u.kind==="branch"?g(n,u,[...r,u.name],a):k(n,u,[...r,u.name]))});return n.provider!==void 0&&(i.effectiveHelp=$(i,n.provider,r.slice(a))),i}o(g,"buildBranch");function k(n,e,r){const a=n.adaptLeaf(e,r);return A({name:e.name,description:e.description,whenToUse:e.whenToUse,...e.tier!==void 0?{tier:e.tier}:{},...n.plugin!==void 0?{plugin:n.plugin}:{},...a.machineSelector===!0?{machineSelector:!0}:{},help:{name:r.join(" "),summary:e.summary,...a.machineSelector===!0?{guide:I}:{},params:e.params,output:e.output,outputKind:a.outputKind,effects:[...e.effects]},run:a.run})}o(k,"buildLeaf");export{ee as adaptPluginContributions,ne as adaptPluginCoreMounts,re as composeCoreBranchMounts,b as composeExternalSubtrees,Y as composeSandboxProviderSubtrees};
|
|
@@ -12,6 +12,10 @@ export interface EndpointValidation {
|
|
|
12
12
|
isValid: boolean;
|
|
13
13
|
errors: string[];
|
|
14
14
|
}
|
|
15
|
+
/** Hosts `http:` is allowed for: loopback, Docker's host alias, and any
|
|
16
|
+
* `*.localhost` name (RFC 6761 reserves it for local use; the local
|
|
17
|
+
* directory and provider stacks serve `http://<svc>.localhost:<port>`). */
|
|
18
|
+
export declare function isLoopbackHostname(hostname: string): boolean;
|
|
15
19
|
export declare function validateEndpoint(endpoint: unknown): EndpointValidation;
|
|
16
20
|
export declare function normalizeEndpoint(endpoint: string): string;
|
|
17
21
|
export declare function validateHttpPluginTransport(raw: unknown): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var p=Object.defineProperty;var e=(n,t)=>p(n,"name",{value:t,configurable:!0});import{validateEnvVarName as i}from"../env-name.js";function h(n){return n==="localhost"||n.endsWith(".localhost")||n==="127.0.0.1"||n==="[::1]"||n==="::1"||n==="host.docker.internal"}e(h,"isLoopbackHostname");function l(n){const t=[];if(typeof n!="string")t.push("endpoint must be a string");else try{const o=new URL(n);(o.protocol!=="https:"&&!(o.protocol==="http:"&&h(o.hostname))||o.username||o.password||o.hash)&&t.push("endpoint must be an absolute https:// URL (http:// permitted for loopback and *.localhost hosts only; no credentials or fragment)")}catch{t.push("endpoint must be an absolute https:// URL (http:// permitted for loopback and *.localhost hosts only; no credentials or fragment)")}return{isValid:t.length===0,errors:t}}e(l,"validateEndpoint");function u(n){try{const t=new URL(n),o=t.pathname.length>1&&t.pathname.endsWith("/")?t.pathname.slice(0,-1):t.pathname;return`${t.protocol}//${t.host}${o}${t.search}`}catch{return n}}e(u,"normalizeEndpoint");function c(n){const t=[];if(typeof n!="object"||n===null||Array.isArray(n))return{errors:["transport must be an object"]};const o=n,r=Object.keys(o).filter(a=>!["kind","endpoint","authEnv"].includes(a));r.length&&t.push(`transport has unknown keys: ${r.join(", ")}`),o.kind!=="http"&&t.push('transport.kind must be "http"');const s=l(o.endpoint);return t.push(...s.errors),o.authEnv!==void 0&&t.push(...i(o.authEnv).errors),t.length?{errors:t}:{transport:{kind:"http",endpoint:u(o.endpoint),...o.authEnv!==void 0?{authEnv:o.authEnv}:{}},errors:t}}e(c,"validateHttpPluginTransport");function m(n){if(typeof n=="object"&&n!==null&&!Array.isArray(n)&&n.kind==="machine"){const t=Object.keys(n).filter(o=>o!=="kind");return t.length?{errors:[`transport has unknown keys: ${t.join(", ")}`]}:{transport:{kind:"machine"},errors:[]}}return c(n)}e(m,"validateBundleTransport");export{h as isLoopbackHostname,u as normalizeEndpoint,m as validateBundleTransport,l as validateEndpoint,c as validateHttpPluginTransport};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ExternalLeafSpec } from './exec-invoke.js';
|
|
2
|
+
export interface AsyncLeafOptions {
|
|
3
|
+
/** Wall-clock budget; the child is SIGKILLed when it runs out. */
|
|
4
|
+
deadlineMs: number;
|
|
5
|
+
cwd: string;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
}
|
|
8
|
+
/** Run one external leaf without blocking, and return its result object. Rejects
|
|
9
|
+
* with `plugin_timeout` at the deadline (child killed), a protocol error on a
|
|
10
|
+
* stdout overrun (child killed), or whatever `parseLeafEnvelope` raises. */
|
|
11
|
+
export declare function executeExternalLeafAsync(spec: ExternalLeafSpec, input: Record<string, unknown>, options: AsyncLeafOptions): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var y=Object.defineProperty;var i=(r,s)=>y(r,"name",{value:s,configurable:!0});import{spawn as I}from"node:child_process";import{CrtrError as a}from"../../errors.js";import{ExitCode as p}from"../../../types.js";import{buildLeafRequest as T,label as R,MAX_STDOUT as k,parseLeafEnvelope as A,protocolError as h}from"./exec-invoke.js";const g=64*1024;function v(r,s,o){const w=T(r,s,o.cwd);return new Promise((L,l)=>{const t=I(r.executable,["--crtr-command-protocol","1"],{cwd:o.cwd,env:o.env,stdio:["pipe","pipe","pipe"],detached:!0}),u=[];let f=0,n=Buffer.alloc(0),c=!1;const m=i(()=>{try{t.pid!==void 0?process.kill(-t.pid,"SIGKILL"):t.kill("SIGKILL")}catch{try{t.kill("SIGKILL")}catch{}}},"kill"),d=i(e=>{c||(c=!0,clearTimeout(S),e())},"finish"),E=i(e=>!(e instanceof a)||e.code!=="plugin_protocol_error"||n.length===0?e:new a(e.code,`${e.message} stderr: ${n.toString("utf8").trim()}`,p.GENERAL,e.details),"withStderr"),S=setTimeout(()=>{m(),d(()=>l(new a("plugin_timeout",`${R(r)}: no answer within ${o.deadlineMs} ms; the process was killed`,p.GENERAL,{next:"Retry, or inspect the plugin with `crtr sys doctor`."})))},o.deadlineMs);t.stdout.on("data",e=>{if(!c){if(f+=e.length,f>k){m(),d(()=>l(h(r,"stdout exceeded the 10 MiB cap","one JSON envelope under 10 MiB on stdout")));return}u.push(e)}}),t.stderr.on("data",e=>{n=Buffer.concat([n,e]),n.length>g&&(n=n.subarray(n.length-g))}),t.on("error",e=>{d(()=>l(h(r,`executable could not be run (${e.code??e.message})`,"a runnable command executable")))}),t.on("close",(e,x)=>{d(()=>{try{L(A(r,Buffer.concat(u).toString("utf8"),e,x))}catch(b){l(E(b))}})}),t.stdin.on("error",()=>{}),t.stdin.end(JSON.stringify(w))})}i(v,"executeExternalLeafAsync");export{v as executeExternalLeafAsync};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { InstalledPlugin } from '../../../types.js';
|
|
2
2
|
import type { Field } from '../../help.js';
|
|
3
|
+
import { CrtrError } from '../../errors.js';
|
|
4
|
+
export declare const MAX_STDOUT: number;
|
|
3
5
|
export type ExternalLeafSpec = {
|
|
4
6
|
plugin: InstalledPlugin;
|
|
5
7
|
extension?: never;
|
|
@@ -9,6 +11,8 @@ export type ExternalLeafSpec = {
|
|
|
9
11
|
commandPath: string[];
|
|
10
12
|
/** Declared leaf output fields — validated against the result before render. */
|
|
11
13
|
output: Field[];
|
|
14
|
+
/** Extra environment for this call, layered over the inherited environment. */
|
|
15
|
+
appEnv?: Record<string, string>;
|
|
12
16
|
} | {
|
|
13
17
|
plugin?: never;
|
|
14
18
|
extension: {
|
|
@@ -21,8 +25,18 @@ export type ExternalLeafSpec = {
|
|
|
21
25
|
commandPath: string[];
|
|
22
26
|
/** Declared leaf output fields — validated against the result before render. */
|
|
23
27
|
output: Field[];
|
|
28
|
+
/** Extra environment for this call, layered over the inherited environment. */
|
|
29
|
+
appEnv?: Record<string, string>;
|
|
24
30
|
};
|
|
31
|
+
/** The one JSON request written to the plugin's stdin. */
|
|
32
|
+
export declare function buildLeafRequest(spec: ExternalLeafSpec, input: Record<string, unknown>, cwd: string): Record<string, unknown>;
|
|
25
33
|
/** Execute one external leaf and return its result object (crtr renders it via
|
|
26
34
|
* the generic renderer; `--json` mirrors it). Throws a CrtrError for any
|
|
27
35
|
* protocol violation or the plugin's own recoverable error. */
|
|
28
36
|
export declare function executeExternalLeaf(spec: ExternalLeafSpec, input: Record<string, unknown>): Record<string, unknown>;
|
|
37
|
+
/** Turn a finished plugin process (stdout text, exit status, terminating signal)
|
|
38
|
+
* into the result object, or throw the plugin's own error or a protocol error.
|
|
39
|
+
* Shared by the synchronous and asynchronous runners. */
|
|
40
|
+
export declare function parseLeafEnvelope(spec: ExternalLeafSpec, stdout: string, status: number | null, signal: NodeJS.Signals | null): Record<string, unknown>;
|
|
41
|
+
export declare function protocolError(spec: ExternalLeafSpec, received: string, expected: string): CrtrError;
|
|
42
|
+
export declare function label(spec: ExternalLeafSpec): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var x=Object.defineProperty;var u=(e,r)=>x(e,"name",{value:r,configurable:!0});import{spawnSync as v}from"node:child_process";import{validateDeclaredResult as $}from"../../../api/command-manifest/result.js";import{CrtrError as f}from"../../errors.js";import{diag as m}from"../../io.js";import{ExitCode as g}from"../../../types.js";import{isRecord as c}from"../../../shared/predicates.js";const E=10*1024*1024,h=new Set(["internal","unknown_path","command_collision","plugin_protocol_error"]),b=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,w="Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plugin.";function S(e,r,t){return{protocolVersion:1,command:e.commandPath,input:r,context:{cwd:t,...e.plugin!==void 0?{plugin:{name:e.plugin.name,version:e.plugin.version??e.plugin.manifest.version??null,scope:e.plugin.scope,root:e.plugin.root}}:{extension:e.extension}}}}u(S,"buildLeafRequest");function D(e,r){const t=S(e,r,process.cwd()),n=v(e.executable,["--crtr-command-protocol","1"],{input:JSON.stringify(t),cwd:process.cwd(),env:{...process.env,...e.appEnv},maxBuffer:E,stdio:["pipe","pipe","inherit"],encoding:"utf8"});if(n.error!==void 0){const o=n.error;throw o.code==="ENOBUFS"||o.code==="ERR_CHILD_PROCESS_STDIO_MAXBUFFER"?i(e,"stdout exceeded the 10 MiB cap","one JSON envelope under 10 MiB on stdout"):i(e,`executable could not be run (${o.code??o.message})`,"a runnable command executable")}return y(e,n.stdout??"",n.status,n.signal)}u(D,"executeExternalLeaf");function y(e,r,t,n){if(n!==null)throw i(e,`executable was killed by signal ${n}`,"a clean exit with one JSON envelope on stdout");let o;try{o=JSON.parse(r.trim())}catch{throw i(e,"stdout was not a single JSON envelope","exactly one JSON envelope on stdout")}if(!c(o))throw i(e,"stdout was not a JSON object","a { protocolVersion: 1, ok: boolean, ... } envelope");if(o.protocolVersion!==1)throw i(e,`unsupported protocolVersion ${String(o.protocolVersion)}`,"protocolVersion exactly 1");if(typeof o.ok!="boolean")throw i(e,'envelope is missing a boolean "ok"',"a boolean ok field");const a=o.ok;if(a&&t!==0?m(`crtr: ${p(e)} returned ok:true but exited ${t} (honoring the envelope)`):!a&&t===0&&m(`crtr: ${p(e)} returned ok:false but exited 0 (honoring the envelope)`),a){const s=o.result;if(!c(s))throw i(e,"success envelope has no result object","result: an object matching the declared output");return k(e,s),s}const l=o.error;if(!c(l)||typeof l.code!="string"||typeof l.message!="string")throw i(e,"error envelope is missing a { code, message }","error: { code, message, ... }");const d=l.code;throw!b.test(d)||h.has(d)?i(e,`error code "${d}" is invalid or reserved`,`a lowercase snake_case code not in {${[...h].join(", ")}}`):new f(d,l.message,g.GENERAL,{received:l.received,field:typeof l.field=="string"?l.field:void 0,next:typeof l.next=="string"?l.next:w})}u(y,"parseLeafEnvelope");function k(e,r){const t=$(e.output,r);if(t===null)return;const n=t.field,o=e.output.find(a=>a.name===n);throw t.receivedType!==void 0?i(e,`output field "${n}" has the wrong type`,`${n}: ${o?.type??"unknown"}`):i(e,`result is missing required output field "${n}"`,`a "${n}" field of type ${o?.type??"unknown"}`)}u(k,"validateResult");function i(e,r,t){return new f("plugin_protocol_error",`${p(e)}: ${r}. Expected ${t}.`,g.GENERAL,{received:r,next:w})}u(i,"protocolError");function p(e){return e.plugin!==void 0?`plugin "${e.plugin.name}" command \`${e.commandPath.join(" ")}\``:`extension branch "${e.extension.branch}" command \`${e.commandPath.join(" ")}\``}u(p,"label");export{E as MAX_STDOUT,S as buildLeafRequest,D as executeExternalLeaf,p as label,y as parseLeafEnvelope,i as protocolError};
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { LeafRunContext } from '../../command.js';
|
|
2
|
-
import type { HttpInvokerSpec } from './http-plan.js';
|
|
2
|
+
import type { HttpInvokerSpec, RequestPlan } from './http-plan.js';
|
|
3
|
+
/** Optional hooks around one call. With none, a call behaves exactly as before. */
|
|
4
|
+
export interface InvokeHttpOptions {
|
|
5
|
+
/** Called once with the final plan; the headers it returns are set last, so a
|
|
6
|
+
* connection retry (which reuses the plan) resends the same values. A
|
|
7
|
+
* rejection fails the call and nothing is sent. */
|
|
8
|
+
callHeaders?: (plan: RequestPlan) => Promise<Record<string, string>>;
|
|
9
|
+
}
|
|
3
10
|
/**
|
|
4
11
|
* Execute one HTTP-transport plugin leaf. Unary leaves return the validated response
|
|
5
12
|
* object (crtr's ordinary dispatcher renders it, or mirrors it under --json).
|
|
@@ -12,4 +19,4 @@ import type { HttpInvokerSpec } from './http-plan.js';
|
|
|
12
19
|
* process.exitCode = NETWORK — it never throws into handle() and never writes a
|
|
13
20
|
* second result/error object to stdout.
|
|
14
21
|
*/
|
|
15
|
-
export declare function invokeHttpLeaf(spec: HttpInvokerSpec, input: Record<string, unknown>, context?: LeafRunContext): Promise<Record<string, unknown> | void>;
|
|
22
|
+
export declare function invokeHttpLeaf(spec: HttpInvokerSpec, input: Record<string, unknown>, context?: LeafRunContext, options?: InvokeHttpOptions): Promise<Record<string, unknown> | void>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var P=Object.defineProperty;var
|
|
2
|
-
`))!==-1;){const
|
|
3
|
-
`)){E(),
|
|
1
|
+
var P=Object.defineProperty;var s=(e,t)=>P(e,"name",{value:t,configurable:!0});import{randomUUID as U}from"node:crypto";import{request as S}from"node:http";import{request as R}from"node:https";import{buildRequestPlan as A,mapUnary2xx as F,throwNon2xx as M}from"./http-plan.js";import{diag as I,recordPreviewError as L,recordPreviewJsonLine as O,writeStdout as x}from"../../io.js";import{ExitCode as _}from"../../../types.js";import{CrtrError as q}from"../../errors.js";const v=1e4,H=3e4,B=6e4,j=250,K=2e3,W=new Set(["ECONNREFUSED","ECONNRESET","EPIPE"]);class k extends Error{static{s(this,"ConnectionError")}detail;constructor(t){super(t),this.detail=t}}class N extends Error{static{s(this,"StreamIdleError")}}async function oe(e,t,i,a){const u=A(e,t,i,{get authorization(){const n=e.registration.authEnv;if(n===void 0||n==="")return null;const r=process.env[n];if(r===void 0||r==="")throw new q("auth_env_missing",`${C(e)}: environment variable ${n} is not set or empty`,_.USAGE,{field:n,received:n,next:`Export ${n} with the bearer token, then retry.`});return`Bearer ${r}`},requestId:U(),extraHeaders:{},fileMode:"read-local"});if(a?.callHeaders!==void 0)for(const[n,r]of Object.entries(await a.callHeaders(u))){const f=Object.keys(u.headers).find($=>$.toLowerCase()===n.toLowerCase());f!==void 0&&delete u.headers[f],u.headers[n]=r}if(e.rest.streaming===!0){await J(e,u);return}const{status:c,body:m}=await Y(e,u);if(c>=200&&c<300)return F(e,m);M(e,c,m)}s(oe,"invokeHttpLeaf");async function Y(e,t){const i=e.timeouts?.requestMs??H,a=Date.now()+i;let u=j;for(;;)try{return await z(e,t,a-Date.now())}catch(c){if(!(c instanceof k))throw c;const m=a-Date.now(),n=y(e,t,`${c.detail}; retried with the same request id until the ${i}ms deadline`);if(m<=0||(await new Promise(r=>setTimeout(r,Math.min(u,m))),Date.now()>=a))throw n;u=Math.min(u*2,K)}}s(Y,"requestUnary");function z(e,t,i){const a=Math.min(e.timeouts?.connectMs??v,i),u=t.url.protocol==="https:"?R:S;return new Promise((c,m)=>{let n=!1,r=null,f=null;const $=s(()=>{r!==null&&clearTimeout(r),f!==null&&clearTimeout(f),r=null,f=null},"clearTimers"),E=s(o=>{n||(n=!0,$(),d.destroy(),m(o))},"fail"),h=s(o=>{n||(n=!0,$(),c(o))},"succeed"),d=u(t.url,{method:t.method},o=>{r!==null&&(clearTimeout(r),r=null);const T=[];o.on("data",w=>T.push(w)),o.on("end",()=>h({status:o.statusCode??0,body:Buffer.concat(T).toString("utf8")})),o.on("error",w=>E(y(e,t,`response interrupted (${b(w)})`))),o.on("close",()=>E(y(e,t,"response closed before the full body")))});for(const[o,T]of Object.entries(t.headers))d.setHeader(o,T);r=setTimeout(()=>E(y(e,t,`connection timed out after ${a}ms`)),a),f=setTimeout(()=>E(y(e,t,`request exceeded the ${i}ms timeout`)),i),d.on("socket",o=>{o.connecting?o.once("connect",()=>l()):l()});const l=s(()=>{r!==null&&(clearTimeout(r),r=null)},"clearConnectTimer");d.on("error",o=>E(G(e,t,o,"could not reach the backend"))),t.body!==void 0&&d.write(t.body),d.end()})}s(z,"requestUnaryOnce");function G(e,t,i,a){const u=b(i),c=`${a} (${u})`;return W.has(u)?new k(c):y(e,t,c)}s(G,"transportFailure");async function J(e,t){const i=e.timeouts?.connectMs??v,a=e.timeouts?.streamIdleMs??B,u=t.url.protocol==="https:"?R:S,c=await new Promise((h,d)=>{let l=!1,o=null;const T=s(()=>{o!==null&&(clearTimeout(o),o=null)},"clearConnect"),w=u(t.url,{method:t.method},g=>{l||(l=!0,T(),h(g))});for(const[g,D]of Object.entries(t.headers))w.setHeader(g,D);o=setTimeout(()=>{l||(l=!0,w.destroy(),d(y(e,t,`connection timed out after ${i}ms`)))},i),w.on("socket",g=>{g.connecting?g.once("connect",T):T()}),w.on("error",g=>{l||(l=!0,T(),d(y(e,t,`could not reach the backend (${b(g)})`)))}),t.body!==void 0&&w.write(t.body),w.end()}),m=c.statusCode??0;if(!(m>=200&&m<300)){const h=await Q(c);M(e,m,h)}let n=!1,r="",f=null;const $=s(()=>{f!==null&&clearTimeout(f),f=setTimeout(()=>c.destroy(new N),a)},"armIdle"),E=s(()=>{f!==null&&(clearTimeout(f),f=null)},"clearIdle");try{$();for await(const h of c){$(),r+=h.toString("utf8");let d;for(;(d=r.indexOf(`
|
|
2
|
+
`))!==-1;){const l=r.slice(0,d);if(r=r.slice(d+1),l.trim()==="")continue;if(n=!0,O(l),!await x(l+`
|
|
3
|
+
`)){E(),c.destroy();return}}}E(),r.trim()!==""&&(n=!0,O(r),await x(r));return}catch(h){E();const d=h instanceof N?`stream idle for ${a}ms`:`connection error (${b(h)})`;if(n){const l=`${C(e)}: stream interrupted (${d})`;I(`crtr: ${l}`),L({error:"stream_interrupted",message:l,next:"Retry the command to begin a fresh stream."}),process.exitCode=_.NETWORK;return}throw y(e,t,d)}}s(J,"requestStreaming");function y(e,t,i){return new q("cli_unreachable",`${C(e)}: ${i}`,_.NETWORK,{received:t.url.origin,next:`Check connectivity to ${t.url.host} and retry; run \`crtr pkg plugin update ${e.registration.name}\` if the command surface changed.`})}s(y,"unreachable");function C(e){return`HTTP-transport plugin "${e.registration.name}" command \`${e.commandPath.join(" ")}\``}s(C,"label");function b(e){const t=e;return t?.code??t?.message??String(e)}s(b,"errCode");function Q(e){return new Promise(t=>{const i=[];e.on("data",a=>i.push(a)),e.on("end",()=>t(Buffer.concat(i).toString("utf8"))),e.on("error",()=>t(Buffer.concat(i).toString("utf8")))})}s(Q,"readAll");export{oe as invokeHttpLeaf};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var R=Object.defineProperty;var f=(e,t)=>R(e,"name",{value:t,configurable:!0});import{readFileSync as O,statSync as k}from"node:fs";import{resolve as j}from"node:path";import{validateDeclaredResult as C}from"../../../api/command-manifest/result.js";import{CrtrError as w}from"../../errors.js";import{envelopeClaimsStale as P,staleErrorDetails as z}from"../../manifest-stale.js";import{ExitCode as h}from"../../../types.js";import{isRecord as x}from"../../../shared/predicates.js";import{trimHeaderValue as U}from"./sign.js";const q=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,T=new Set(["internal","unknown_path","command_collision","cli_protocol_error"]),D=new Set(["authorization","crtr-request-id","crtr-run-id","crtr-approval","crtr-upload-id","crtr-upload-link","accept","content-type","crtr-caller","crtr-signature"]),M=new Set(["crtr-caller","crtr-signature","crtr-request-id"]);function te(e,t,n,r){const i=new Map;for(const o of e.params)i.set(o.name,o);const c=Object.entries(e.rest.params),u=e.baseUrl??new URL(e.registration.endpoint).origin;let y=e.rest.path;for(const[o,a]of c){if(a.in!=="path")continue;const d=t[S(o)];y=y.split(`{${o}}`).join(I(d))}const b=new URL(y,u);for(const[o,a]of c){if(a.in!=="query")continue;const d=S(o);v(i.get(o),d,t,n)&&b.searchParams.append(a.as??o,String(t[d]))}const m=G(e,c,i,t,n,r.fileMode),p=Object.keys(m).length>0?JSON.stringify(m):void 0,l={},s=f((o,a)=>{const d=Object.keys(l).find(g=>g.toLowerCase()===o.toLowerCase());d!==void 0&&delete l[d],l[o]=U(a)},"setHeader");s("Accept","application/json"),s("Crtr-Request-Id",r.requestId),r.authorization!==null&&s("Authorization",r.authorization);for(const[o,a]of c){if(a.in!=="header")continue;const d=S(o);if(!v(i.get(o),d,t,n))continue;const g=a.as;if(M.has(g.toLowerCase())||r.fileMode==="pass-through"&&D.has(g.toLowerCase()))throw new w("invalid_type",`${E(e)}: header "${g}" is reserved for the runtime`,h.USAGE,{field:o,next:"Remove the runtime-owned header mapping from the manifest."});const A=String(t[d]);if(/[\r\n]/.test(A))throw new w("invalid_type",`${E(e)}: header "${g}" value must not contain a newline`,h.USAGE,{field:o,received:"(newline in header value)",next:"Remove newline characters from the value."});s(g,A)}p!==void 0&&s("Content-Type","application/json");for(const[o,a]of Object.entries(r.extraHeaders)){if(r.fileMode==="pass-through"&&/[\r\n]/.test(a))throw new w("invalid_type",`${E(e)}: header "${o}" value must not contain a newline`,h.USAGE);s(o,a)}if(r.fileMode==="pass-through"){if(s("Accept","application/json"),p!==void 0)s("Content-Type","application/json");else{const o=Object.keys(l).find(a=>a.toLowerCase()==="content-type");o!==void 0&&delete l[o]}if(s("Crtr-Request-Id",r.requestId),r.authorization!==null)s("Authorization",r.authorization);else{const o=Object.keys(l).find(a=>a.toLowerCase()==="authorization");o!==void 0&&delete l[o]}}return{url:b,method:e.rest.method,headers:l,...p!==void 0?{body:p}:{}}}f(te,"buildRequestPlan");function G(e,t,n,r,i,c){const u=new Map;for(const[m,p]of t){if(p.in!=="body")continue;const l=p.as??m,s=u.get(l);s!==void 0?s.push(m):u.set(l,[m])}const y={};for(const[m,p]of u){const l=p.map(d=>({name:d,param:n.get(d)})),s=l.find(d=>d.param.kind!=="stdin"),o=l.find(d=>d.param.kind==="stdin");let a;s!==void 0&&v(s.param,S(s.name),r,i)?a={param:s.param,value:r[S(s.name)]}:o!==void 0&&v(o.param,S(o.name),r,i)&&(a={param:o.param,value:r[S(o.name)]}),a!==void 0&&(y[m]=H(e,a.param,a.value,c))}const b={...e.rest.body};return e.rest.bodyRoot!==void 0?b[e.rest.bodyRoot]=y:Object.assign(b,y),b}f(G,"assembleBody");function H(e,t,n,r){return r==="pass-through"||t.kind==="context-file"?n:(t.kind==="positional"||t.kind==="flag")&&t.type==="path"?N(E(e),t,n):n}f(H,"materializeBodyValue");function N(e,t,n){if(Array.isArray(n))return n.map(u=>N(e,t,u));const r=String(n),i=j(process.cwd(),r);let c;try{c=k(i)}catch{throw _(e,t.name,r,"no file exists at that path","an existing local file path")}if(!c.isFile())throw _(e,t.name,r,"the path is not a regular file","a regular local file (not a directory, socket, or device)");try{return"encoding"in t&&t.encoding==="base64"?O(i).toString("base64"):O(i,"utf8")}catch{throw _(e,t.name,r,"the file could not be read","a readable local file path")}}f(N,"readPathParam");function _(e,t,n,r,i){return new w("invalid_type",`${e}: cannot read file for "${t}" \u2014 ${r}: ${n}`,h.USAGE,{field:t,received:n,next:`Pass ${i}, relative to the current directory or absolute.`})}f(_,"fileError");function v(e,t,n,r){return r!==void 0?r.providedParams.has(t):Object.prototype.hasOwnProperty.call(n,t)&&n[t]!==void 0}f(v,"isPresent");function I(e){const t=String(e);return t==="."||t===".."?t.replace(/\./g,"%2E"):encodeURIComponent(t)}f(I,"encodePathSegmentValue");function S(e){return e.replace(/-([a-z])/g,(t,n)=>n.toUpperCase())}f(S,"paramKey");function ne(e,t){let n;try{n=JSON.parse(t)}catch{throw $(e,"response body was not valid JSON","a JSON object matching the declared output")}if(!x(n))throw $(e,`response body was not a JSON object (got ${V(n)})`,"a JSON object matching the declared output");const r=C(e.output,n);if(r!==null){const c=e.output.find(u=>u.name===r.field)?.type??"unknown";throw r.receivedType!==void 0?$(e,`output field "${r.field}" has the wrong type`,`${r.field}: ${c}`):$(e,`response is missing required output field "${r.field}"`,`a "${r.field}" field of type ${c}`)}return n}f(ne,"mapUnary2xx");function re(e,t,n){const r=L(t),i=J(n);if(i!==null){const c=q.test(i.code)&&!T.has(i.code);throw new w(c?i.code:"cli_protocol_error",i.message,r,{...i.received!==void 0?{received:i.received}:{},...i.field!==void 0?{field:i.field}:{},...i.manifestStale?z(e.registration.name):{},http_status:t,next:i.next??"Inspect the backend response, then retry if appropriate."})}throw new w("cli_http_error",`${E(e)}: request failed with HTTP ${t}`,r,{http_status:t,next:"Inspect the backend response, then retry if appropriate."})}f(re,"throwNon2xx");function J(e){let t;try{t=JSON.parse(e)}catch{return null}if(!x(t)||!x(t.error))return null;const n=t.error;return typeof n.code!="string"||typeof n.message!="string"?null:{code:n.code,message:n.message,...typeof n.field=="string"?{field:n.field}:{},...n.received!==void 0?{received:n.received}:{},...typeof n.next=="string"?{next:n.next}:{},manifestStale:P(n),...n.question!==void 0?{question:n.question}:{}}}f(J,"parseHttpError");function L(e){return e===400||e===401||e===403||e===422?h.USAGE:e===404?h.NOT_FOUND:e===409?h.AMBIGUOUS:h.GENERAL}f(L,"deriveExit");function $(e,t,n){return new w("cli_protocol_error",`${E(e)}: ${t}. Expected ${n}.`,h.GENERAL,{received:t,next:`Run \`crtr pkg plugin update ${e.registration.name}\` or inspect the backend response.`})}f($,"protocolError");function E(e){return`HTTP-transport plugin "${e.registration.name}" command \`${e.commandPath.join(" ")}\``}f(E,"label");function V(e){return e===null?"null":Array.isArray(e)?"array":typeof e}f(V,"typeName");export{te as buildRequestPlan,ne as mapUnary2xx,J as parseHttpError,N as readPathParam,re as throwNon2xx};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Skew the verifier tolerates between the signature's `t` and its own clock. */
|
|
2
|
+
export declare const SIGNATURE_MAX_SKEW_SECONDS = 300;
|
|
3
|
+
/** How long a verifier must remember a `Crtr-Request-Id`. */
|
|
4
|
+
export declare const SIGNATURE_NONCE_TTL_SECONDS = 600;
|
|
5
|
+
/** A header value without its leading and trailing optional whitespace (spaces and tabs): the form every HTTP receiver
|
|
6
|
+
* sees, since parsers drop it. Signing and sending both use this form, so the plugin hashes what crtr signed. */
|
|
7
|
+
export declare function trimHeaderValue(value: string): string;
|
|
8
|
+
export interface SignedHeaderDigest {
|
|
9
|
+
/** Lowercase names of every signed header, sorted. */
|
|
10
|
+
header_names: string[];
|
|
11
|
+
/** Lowercase hex sha256 of `<name>:<value>\n` for each name in `header_names`. */
|
|
12
|
+
headers_sha256: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CrtrSignatureInput extends SignedHeaderDigest {
|
|
15
|
+
token: string;
|
|
16
|
+
method: string;
|
|
17
|
+
/** The request URL, or its path-with-query. */
|
|
18
|
+
url: URL | string;
|
|
19
|
+
/** Lowercase hex sha256 of the exact body bytes sent (of "" when none). */
|
|
20
|
+
body_sha256: string;
|
|
21
|
+
/** The `Crtr-Caller` value, or '' when none is sent. */
|
|
22
|
+
caller: string;
|
|
23
|
+
/** Epoch milliseconds. */
|
|
24
|
+
now: number;
|
|
25
|
+
}
|
|
26
|
+
export interface VerifyCrtrSignatureInput {
|
|
27
|
+
token: string;
|
|
28
|
+
/** The received `Crtr-Signature` header value. */
|
|
29
|
+
header: string;
|
|
30
|
+
method: string;
|
|
31
|
+
/** The received request target (`/path?query`) or absolute URL. */
|
|
32
|
+
url: URL | string;
|
|
33
|
+
/** The exact body bytes received; undefined or empty when none. */
|
|
34
|
+
body: Uint8Array | string | undefined;
|
|
35
|
+
/** The received `Crtr-Caller` value, or ''. */
|
|
36
|
+
caller: string;
|
|
37
|
+
/** Request headers as received (names in any case). */
|
|
38
|
+
headers: Record<string, string | string[] | undefined>;
|
|
39
|
+
/** Epoch milliseconds. */
|
|
40
|
+
now: number;
|
|
41
|
+
/** Reports whether the nonce was already seen, and records it. */
|
|
42
|
+
seen: (requestId: string) => boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function sha256Hex(data: Uint8Array | string | undefined): string;
|
|
45
|
+
/** Names and digest of the headers a signature covers: every header except
|
|
46
|
+
* `Crtr-Caller` and `Crtr-Signature`. Each value is hashed without its leading and trailing spaces and tabs, the form a
|
|
47
|
+
* receiver gets after its HTTP parser trims them (the request plan sends the same trimmed value). */
|
|
48
|
+
export declare function signedHeaderDigest(headers: Record<string, string>): SignedHeaderDigest;
|
|
49
|
+
/** The `Crtr-Signature` header value. Refuses a header list that is not sorted
|
|
50
|
+
* lowercase names including `crtr-request-id`, so a signature always covers the nonce. */
|
|
51
|
+
export declare function crtrSignature(input: CrtrSignatureInput): string;
|
|
52
|
+
/** Reference verifier for a plugin's service. True only when the version, clock
|
|
53
|
+
* window, signed header list, MAC and nonce all hold. The nonce is recorded
|
|
54
|
+
* only after the MAC verifies, so a forged request cannot poison the cache. */
|
|
55
|
+
export declare function verifyCrtrSignature(input: VerifyCrtrSignatureInput): boolean;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var N=Object.defineProperty;var c=(e,t)=>N(e,"name",{value:t,configurable:!0});import{createHash as _,createHmac as E,timingSafeEqual as y}from"node:crypto";const C=300,D=600,d=new Set(["crtr-caller","crtr-signature"]),f="crtr-request-id";function v(e){return e.replace(/^[ \t]+|[ \t]+$/g,"")}c(v,"trimHeaderValue");function $(e){return _("sha256").update(e??"").digest("hex")}c($,"sha256Hex");function w(e,t){const s=_("sha256");for(const r of e)s.update(`${r}:${t(r)}
|
|
2
|
+
`);return s.digest("hex")}c(w,"headersDigest");function H(e){const t=new Map;for(const[r,i]of Object.entries(e)){const a=r.toLowerCase();d.has(a)||t.set(a,v(i))}const s=[...t.keys()].sort();return{header_names:s,headers_sha256:w(s,r=>t.get(r))}}c(H,"signedHeaderDigest");function S(e){if(typeof e!="string")return e.pathname+e.search;if(e.startsWith("/"))return e;const t=new URL(e);return t.pathname+t.search}c(S,"target");function b(e){return["v1",String(e.t),e.method.toUpperCase(),e.target,e.body_sha256,e.caller,e.h,e.headers_sha256].join(`
|
|
3
|
+
`)}c(b,"canonical");function x(e,t){return E("sha256",e).update(t).digest()}c(x,"mac");function L(e){const t=e.header_names;if(!t.includes(f))throw new Error("signed headers must include crtr-request-id");for(let a=0;a<t.length;a++){const o=t[a];if(o!==o.toLowerCase()||/[;,\s]/.test(o)||d.has(o)||a>0&&t[a-1]>=o)throw new Error("signed header names must be unique, sorted, lowercase, and exclude crtr-caller and crtr-signature")}const s=Math.floor(e.now/1e3),r=t.join(";"),i=x(e.token,b({t:s,method:e.method,target:S(e.url),body_sha256:e.body_sha256,caller:e.caller,h:r,headers_sha256:e.headers_sha256})).toString("base64url");return`v1,t=${s},h=${r},sig=${i}`}c(L,"crtrSignature");function M(e){const t=e.header.split(",");if(t.length!==4||t[0]!=="v1")return!1;const[s,r,i]=[t[1],t[2],t[3]].map((n,h)=>{const m=["t=","h=","sig="][h];return n.startsWith(m)?n.slice(m.length):null});if(s===null||r===null||i===null||!/^\d{1,12}$/.test(s))return!1;const a=Number(s);if(Math.abs(e.now/1e3-a)>C)return!1;const o=r.split(";");if(!o.includes(f))return!1;const l=new Map;for(const[n,h]of Object.entries(e.headers))typeof h=="string"&&l.set(n.toLowerCase(),h);for(let n=0;n<o.length;n++){const h=o[n];if(h===""||d.has(h)||!l.has(h)||n>0&&o[n-1]>=h)return!1}const u=x(e.token,b({t:a,method:e.method,target:S(e.url),body_sha256:$(e.body),caller:e.caller,h:r,headers_sha256:w(o,n=>l.get(n))})),g=Buffer.from(i,"base64url");return g.length!==u.length||!y(g,u)?!1:!e.seen(l.get(f))}c(M,"verifyCrtrSignature");export{C as SIGNATURE_MAX_SKEW_SECONDS,D as SIGNATURE_NONCE_TTL_SECONDS,L as crtrSignature,$ as sha256Hex,H as signedHeaderDigest,v as trimHeaderValue,M as verifyCrtrSignature};
|
|
@@ -17,6 +17,25 @@ export declare function toolApps(db: DatabaseSync, reader: Reader): ObjectRow[];
|
|
|
17
17
|
export declare function mayUseTools(_db: DatabaseSync, _reader: Reader, app: ObjectRow): boolean;
|
|
18
18
|
/** Whether this reader may register, publish or watch as an object owned by `owner`. */
|
|
19
19
|
export declare function mayActAs(db: DatabaseSync, reader: Reader, owner: ObjectRow): boolean;
|
|
20
|
+
/** Run confinement for a node acting on a custom object of its app (Silas, 2026-09-30):
|
|
21
|
+
* a node reaches an object registered in its own run, or when neither run is isolated.
|
|
22
|
+
* An object the person or the app's server registered is reached by nodes outside
|
|
23
|
+
* isolation only. Any other reader is unchanged. */
|
|
24
|
+
export declare function customReachable(db: DatabaseSync, reader: Reader, object: ObjectRow): boolean;
|
|
25
|
+
/** The `crtr:objects:*` targets an object belongs to, from its owner chain (rulings R5):
|
|
26
|
+
* person space → `user`; under an app's profile → `profile:<id>`; a node of an app or a custom
|
|
27
|
+
* object → `app:<x>`; any node (local, or a stub of one of the account's nodes) → `node`.
|
|
28
|
+
* App documents and the profile itself stay under the memory scopes; a repo has no target.
|
|
29
|
+
* The app's own `app:<x>` is left out: its own objects need no scope. */
|
|
30
|
+
export declare function objectsTargets(db: DatabaseSync, object: ObjectRow, appGrantee: string): string[];
|
|
31
|
+
/** Whether a non-person reader's scopes hold `crtr:objects:<verb>:<t>` for a target t of `object`.
|
|
32
|
+
* False for a person and for a revoked grant (`scopes === null`). `covers` is exact equality here. */
|
|
33
|
+
export declare function holdsObjectsScope(db: DatabaseSync, reader: Reader, verb: 'read' | 'watch' | 'link' | 'write', object: ObjectRow): boolean;
|
|
34
|
+
/** Whether the watch `watcher` → `object` that `app` made through `link` still authorizes delivering `object`'s events to `watcher` past the
|
|
35
|
+
* watcher's read gate: `app` owns `object` (directly, or through its profile: `mayActAs`) and, when its `scopes` are known, they hold
|
|
36
|
+
* `crtr:objects:link:<t>` for the watcher. Null `scopes` (a stub of a remote object: the home already checked the link) skip the scope. It grants
|
|
37
|
+
* delivery of this one object's events only; nothing about reading it. */
|
|
38
|
+
export declare function linkAuthorizes(db: DatabaseSync, app: ObjectRow, scopes: readonly string[] | null, object: ObjectRow, watcher: ObjectRow): boolean;
|
|
20
39
|
/** Callers turn `false` into `not_found`. A person reader reads everything. An app's
|
|
21
40
|
* shipped documents are read by any run that may use that app's tools. */
|
|
22
41
|
export declare function canRead(db: DatabaseSync, reader: Reader, object: ObjectRow): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var
|
|
1
|
+
var O=Object.defineProperty;var s=(e,t)=>O(e,"name",{value:t,configurable:!0});import{prepared as a}from"../sqlite-statements.js";import{covers as R}from"@north-light/identity";import{objectPlace as N,runPlace as L,runsSeeEachOther as g}from"../runs/confinement.js";import{missingOwnMemoryScope as E}from"./spaces/own-memory.js";import{SPACES as x,identityOf as f}from"./spaces/index.js";import{insideRepo as C}from"./spaces/repo.js";function T(e,t){return a(e,`WITH RECURSIVE chain(rowid, depth) AS (
|
|
2
2
|
SELECT owner, 1 FROM objects WHERE rowid = ? AND owner IS NOT NULL
|
|
3
3
|
UNION ALL
|
|
4
4
|
SELECT o.owner, c.depth + 1 FROM objects o JOIN chain c ON o.rowid = c.rowid WHERE o.owner IS NOT NULL
|
|
5
|
-
) SELECT o.* FROM chain c CROSS JOIN objects o WHERE o.rowid = c.rowid ORDER BY c.depth`).all(t.rowid)}
|
|
5
|
+
) SELECT o.* FROM chain c CROSS JOIN objects o WHERE o.rowid = c.rowid ORDER BY c.depth`).all(t.rowid)}s(T,"ownerChain");function v(e,t){return c(e,t).space}s(v,"spaceOf");function c(e,t){const n=T(e,t),p=n.length?n[n.length-1]:t,o=x[p.type];if(!o)throw new Error(`${t.type} ${t.id} has no space: its owner chain ends at ${p.type} ${p.id}`);const r=n.length?[...n.slice(0,-1).reverse(),t]:[];return{space:p,path:r,impl:o}}s(c,"locate");function m(e,t){return t.type!=="document"?!1:a(e,"SELECT source_path FROM documents WHERE object = ?").get(t.rowid)?.source_path!=null}s(m,"isShipped");function J(e,t){return a(e,`SELECT a.* FROM objects a WHERE a.type = 'app' AND a.deleted_at IS NULL AND EXISTS (
|
|
6
6
|
SELECT 1 FROM objects o JOIN documents d ON d.object = o.rowid
|
|
7
7
|
WHERE o.owner = a.rowid AND o.type = 'document' AND o.deleted_at IS NULL AND d.source_path IS NOT NULL)
|
|
8
|
-
ORDER BY a.name = 'crtr', a.name`).all().filter(p=>
|
|
8
|
+
ORDER BY a.name = 'crtr', a.name`).all().filter(p=>h(e,t,p))}s(J,"toolApps");function h(e,t,n){return n.type==="app"}s(h,"mayUseTools");function A(e,t,n){return t.class==="person"?!0:t.class==="object"?t.owner.rowid===n.rowid:t.class==="app"?t.app.rowid===n.rowid||n.type==="profile"&&n.owner===t.app.rowid:t.app.rowid===n.rowid||n.type==="profile"&&t.profile?.rowid===n.rowid||n.type==="repo"&&C(e,t,n,"rw")}s(A,"mayActAs");function I(e,t,n){if(t.class!=="node"&&!(t.class==="app"&&t.place!==void 0))return!0;const p=t.class==="app"?t.place:L(t.node.id,e);if(p===null)return!0;const o=N(n.rowid,e);return o===null?!p.isolated:g(p,o)}s(I,"customReachable");const w=s(e=>e.type.includes("."),"isCustom");function P(e,t,n){return y(e,c(e,t),t,n)}s(P,"objectsTargets");function y(e,{space:t,path:n},p,o){const r=[];if(t.type==="person")r.push("user");else if(t.type==="app"&&n.length>0){const i=n[0];if(i.type==="profile")n.length>1&&r.push(`profile:${f(e,i).external_id}`);else if(i.type==="node"||w(i)){const u=f(e,t).external_id;u!==o&&r.push(u)}}return p.type==="node"&&r.push("node"),r}s(y,"targetsOf");function l(e,t,n,p,o){if(t.class==="person"||t.scopes===null)return!1;const r=t.scopes;if(r.length===0||p.space.type==="app"&&t.app.rowid===p.space.rowid)return!1;const i=f(e,t.app).external_id;return y(e,p,o,i).some(u=>{const S=`crtr:objects:${n}:${u}`;return r.some(d=>R(d,S))})}s(l,"holdsInLocated");function M(e,t,n,p){return t.class==="person"||t.scopes===null?!1:l(e,t,n,c(e,p),p)}s(M,"holdsObjectsScope");function B(e,t,n,p,o){const r=p.owner===null?null:e.prepare("SELECT * FROM objects WHERE rowid = ?").get(p.owner)??null;if(r===null)return!1;const i={class:"app",app:t,scopes:n??[]};return A(e,i,r)?n===null||M(e,i,"link",o):!1}s(B,"linkAuthorizes");function Y(e,t,n){return _(e,n)(t)}s(Y,"canRead");function _(e,t){let n=null,p=null;return o=>{if(o.class==="person")return!0;if(n??=c(e,t),n.space.type==="app"&&n.path.length>0){if(p??=m(e,t),p&&h(e,o,n.space))return!0;if(w(t)){if(o.app.rowid===n.space.rowid&&!I(e,o,t))return!1;if(t.privacy==="public"&&o.app.rowid!==n.space.rowid&&o.scopes!==null&&h(e,o,n.space))return!0}}return l(e,o,"read",n,t)||o.class==="object"&&w(o.object)&&l(e,o,"watch",n,t)?!0:n.impl.canRead(e,o,n.space,n.path,t)}}s(_,"readCheck");function z(e,t,n){if(m(e,n))return!1;const p=c(e,n);if(l(e,t,"write",p,n))return!0;const{space:o,path:r,impl:i}=p;return i.canChange(e,t,o,r,n)}s(z,"canChange");function V(e,t,n,p){if(t.class==="person")return null;const{space:o,path:r}=c(e,n);return o.type==="app"&&t.app.rowid===o.rowid?E(e,t,o,r,n,p):null}s(V,"missingMemoryScope");function X(e,t,n,p){if(t.class==="person")return null;const o=c(e,n),{space:r}=o;return r.type!=="app"||t.app.rowid!==r.rowid?null:E(e,t,r,o.path.length===0?[n]:[...o.path],null,p)}s(X,"missingMemoryScopeUnder");export{z as canChange,Y as canRead,I as customReachable,M as holdsObjectsScope,m as isShipped,B as linkAuthorizes,A as mayActAs,h as mayUseTools,V as missingMemoryScope,X as missingMemoryScopeUnder,P as objectsTargets,T as ownerChain,_ as readCheck,v as spaceOf,J as toolApps};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import type { Level, ObjectRow, Reader } from './types.js';
|
|
3
|
-
import { mayActAs } from './access.js';
|
|
2
|
+
import type { Level, ObjectRow, Privacy, Reader } from './types.js';
|
|
3
|
+
import { customReachable, mayActAs } from './access.js';
|
|
4
4
|
import { onQueueSignal } from './receivers.js';
|
|
5
|
-
export { mayActAs, onQueueSignal };
|
|
5
|
+
export { customReachable, mayActAs, onQueueSignal };
|
|
6
6
|
export type CustomReceiver = 'queue' | {
|
|
7
7
|
url: string;
|
|
8
8
|
} | {
|
|
@@ -39,15 +39,40 @@ export declare function registerCustomObject(db: DatabaseSync, reader: Reader, i
|
|
|
39
39
|
name: string;
|
|
40
40
|
owner: ObjectRow;
|
|
41
41
|
description: string;
|
|
42
|
-
|
|
42
|
+
/** Omitted: create or return only the instance of a kind an installed plugin (or a peer) declares; the catalog is untouched. */
|
|
43
|
+
events?: CustomEvents;
|
|
43
44
|
receiver?: CustomReceiver | null;
|
|
44
45
|
rotateToken?: boolean;
|
|
46
|
+
privacy?: Privacy;
|
|
45
47
|
}): {
|
|
46
48
|
object: ObjectRow;
|
|
47
49
|
token?: string;
|
|
48
50
|
created: boolean;
|
|
49
51
|
changed: string[];
|
|
50
52
|
};
|
|
53
|
+
export type KindSource = 'register' | 'manifest' | 'peer';
|
|
54
|
+
export type KindChange = 'events' | 'description' | 'read';
|
|
55
|
+
/**
|
|
56
|
+
* The only writer of `object_kinds`. `source` says who is writing: an app's `register` call, an installed plugin's
|
|
57
|
+
* manifest, or a declaration that crossed from another machine (`peer`). Precedence: `manifest` and `register` rows
|
|
58
|
+
* are never overwritten by `peer`; a `manifest` write takes over a row it already owns and replaces a `peer` row;
|
|
59
|
+
* a `register` write leaves a `manifest` row alone (`skipped`) and replaces a `peer` row; another owner's
|
|
60
|
+
* `manifest` or `register` row is `kind_owned`. `read_json` is written only by `manifest`. A catalog is replaced,
|
|
61
|
+
* never merged. Runs in the caller's write transaction.
|
|
62
|
+
*/
|
|
63
|
+
export declare function upsertKind(db: DatabaseSync, input: {
|
|
64
|
+
name: string;
|
|
65
|
+
owner: ObjectRow;
|
|
66
|
+
description: string;
|
|
67
|
+
events: CustomEvents;
|
|
68
|
+
read: {
|
|
69
|
+
command: string[];
|
|
70
|
+
} | null;
|
|
71
|
+
source: KindSource;
|
|
72
|
+
}): {
|
|
73
|
+
outcome: 'inserted' | 'updated' | 'unchanged' | 'skipped';
|
|
74
|
+
changed: KindChange[];
|
|
75
|
+
};
|
|
51
76
|
export declare function publishCustom(db: DatabaseSync, object: ObjectRow, actor: ObjectRow | null, input: {
|
|
52
77
|
event: string;
|
|
53
78
|
summary: string;
|
|
@@ -67,8 +92,3 @@ export declare function acknowledge(db: DatabaseSync, object: ObjectRow, deliver
|
|
|
67
92
|
unknown?: string[];
|
|
68
93
|
};
|
|
69
94
|
export declare function verifyPublishToken(db: DatabaseSync, object: ObjectRow, token: string): boolean;
|
|
70
|
-
/** Run confinement for a node acting on a custom object of its app (Silas, 2026-09-30):
|
|
71
|
-
* a node reaches an object registered in its own run, or when neither run is isolated.
|
|
72
|
-
* An object the person or the app's server registered is reached by nodes outside
|
|
73
|
-
* isolation only. Any other reader is unchanged. */
|
|
74
|
-
export declare function customReachable(db: DatabaseSync, reader: Reader, object: ObjectRow): boolean;
|