@north-light/cli 0.3.446 → 0.3.447

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/tree-cache.json +728 -0
  2. package/dist/clients/attach/viewer.js +560 -559
  3. package/dist/core/canvas/migrations.js +65 -31
  4. package/dist/core/graph/events.js +6 -6
  5. package/dist/core/graph/kinds/node.js +5 -5
  6. package/dist/core/graph/objects.js +8 -8
  7. package/dist/core/graph/reader.js +3 -3
  8. package/dist/core/graph/receivers.d.ts +2 -1
  9. package/dist/core/graph/receivers.js +5 -5
  10. package/dist/core/graph/spaces/app.js +1 -1
  11. package/dist/core/graph/types.d.ts +9 -1
  12. package/dist/core/peer/ingest.d.ts +26 -0
  13. package/dist/core/peer/ingest.js +1 -0
  14. package/dist/core/peer/outbox-store.d.ts +70 -0
  15. package/dist/core/peer/outbox-store.js +4 -0
  16. package/dist/core/peer/receiver.d.ts +8 -0
  17. package/dist/core/peer/receiver.js +1 -0
  18. package/dist/core/peer/stubs.d.ts +51 -0
  19. package/dist/core/peer/stubs.js +1 -0
  20. package/dist/daemon/api/app-listener.js +9 -9
  21. package/dist/daemon/api/principal.d.ts +4 -4
  22. package/dist/daemon/api/principal.js +1 -1
  23. package/dist/daemon/api/sync-notice.d.ts +17 -0
  24. package/dist/daemon/api/sync-notice.js +1 -0
  25. package/dist/daemon/crtrd.js +8 -8
  26. package/dist/daemon/peer/handlers.d.ts +4 -1
  27. package/dist/daemon/peer/handlers.js +1 -1
  28. package/dist/daemon/peer/local-ingress.d.ts +8 -0
  29. package/dist/daemon/peer/local-ingress.js +1 -0
  30. package/dist/daemon/peer/outbox.d.ts +38 -0
  31. package/dist/daemon/peer/outbox.js +1 -0
  32. package/dist/daemon/peer/relay-client.d.ts +35 -0
  33. package/dist/daemon/peer/relay-client.js +1 -0
  34. package/dist/daemon/peer/relay-protocol.d.ts +52 -0
  35. package/dist/daemon/peer/relay-protocol.js +1 -0
  36. package/dist/daemon/peer/relay-service.d.ts +26 -0
  37. package/dist/daemon/peer/relay-service.js +1 -0
  38. package/package.json +2 -2
  39. package/packages/crouter-identity/package.json +1 -1
  40. package/runtime.lock.json +12 -12
@@ -0,0 +1,8 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ export declare const peerSocketPath: () => string;
3
+ export interface LocalIngress {
4
+ readonly socketPath: string;
5
+ close(): Promise<void>;
6
+ }
7
+ /** Bind `socketPath` (replacing a stale file) and serve every request with `serve`. Resolves once listening and chmodded 0600. */
8
+ export declare function startLocalIngress(serve: (req: IncomingMessage, res: ServerResponse) => Promise<void>, socketPath?: string): Promise<LocalIngress>;
@@ -0,0 +1 @@
1
+ var f=Object.defineProperty;var i=(c,r)=>f(c,"name",{value:r,configurable:!0});import{chmodSync as s,existsSync as t,unlinkSync as m}from"node:fs";import{createServer as l}from"node:http";import{join as p}from"node:path";import{runtimeRoot as a}from"../../core/layout.js";const y=i(()=>p(a(),"peer.sock"),"peerSocketPath");async function x(c,r=y()){const e=l((n,o)=>{c(n,o).catch(()=>{o.headersSent||o.writeHead(500),o.end()})});try{t(r)&&m(r)}catch{}return await new Promise((n,o)=>{e.once("error",o),e.listen(r,()=>{e.off("error",o),n()})}),s(r,384),{socketPath:r,close:i(()=>new Promise(n=>{e.close(()=>{try{t(r)&&m(r)}catch{}n()}),e.closeAllConnections()}),"close")}}i(x,"startLocalIngress");export{y as peerSocketPath,x as startLocalIngress};
@@ -0,0 +1,38 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { type PeerRequest, type PeerResult } from '../../core/peer/client.js';
3
+ import { type Machine } from '../../core/peer/directory.js';
4
+ export declare const STABLE_BACKOFF_MS: {
5
+ readonly first: 1000;
6
+ readonly max: number;
7
+ };
8
+ /** A relay-reach peer is retried for this long after its first failure, then left for its own pull. */
9
+ export declare const RELAY_RETRY_WINDOW_MS: number;
10
+ export declare const PULL_INTERVAL_MS: number;
11
+ export interface OutboxDeps {
12
+ db(): DatabaseSync;
13
+ call(machineId: string, request: PeerRequest): Promise<PeerResult>;
14
+ reach(machineId: string): Promise<Machine | null>;
15
+ machines(): Promise<Machine[]>;
16
+ self(): string;
17
+ now(): number;
18
+ }
19
+ export declare const liveOutboxDeps: OutboxDeps;
20
+ /** The wait before attempt number `attempts` (already made) retries: 1 s doubling to 10 min. */
21
+ export declare const stableBackoffMs: (attempts: number) => number;
22
+ export interface OutboxLane {
23
+ /** Push every due row. Called from the daemon's tick. */
24
+ run(): Promise<void>;
25
+ /** Push what is pending to one peer now (a row was just enqueued). */
26
+ flush(peer: string): Promise<void>;
27
+ /** Claim and apply what every other machine holds for this one. Only a relay-reach machine needs it. */
28
+ pull(): Promise<number>;
29
+ }
30
+ export declare function createOutboxLane(deps: OutboxDeps): OutboxLane;
31
+ /** Start the lane in the daemon: push on enqueue and on a short timer, pull on relay connect, at boot and every 5 minutes.
32
+ * `joined` is false for a runtime with no directory identity (nothing to push to or pull from). Returns the stop function. */
33
+ export declare function startPeerOutbox(opts: {
34
+ joined: boolean;
35
+ register(work: Promise<void>): void;
36
+ deps?: OutboxDeps;
37
+ tickMs?: number;
38
+ }): () => void;
@@ -0,0 +1 @@
1
+ var T=Object.defineProperty;var f=(e,d)=>T(e,"name",{value:d,configurable:!0});import{openDb as I,withCanvasWrite as L}from"../../core/canvas/db.js";import{emitEvent as R}from"../../core/events/emit.js";import{isUnreachable as A,peerCall as j}from"../../core/peer/client.js";import{listMachines as B,machineReach as C,normalizeMachineId as k,ownMachineId as D}from"../../core/peer/directory.js";import{relayConnected as U}from"../../core/peer/events.js";import{ingestPeerEvents as F}from"../../core/peer/ingest.js";import{dueRows as N,expireOld as W,markDone as q,markExpired as O,markRetry as x,markWaiting as S,onPeerEnqueued as X,pendingFor as Y,pruneOutbox as z,OUTBOX_MAX_AGE_MS as G}from"../../core/peer/outbox-store.js";const g={first:1e3,max:10*6e4},E=5*6e4,H=5*6e4,P=50,J=20,K={db:I,call:f((e,d)=>j(e,d),"call"),reach:C,machines:B,self:D,now:Date.now},M=f(e=>Math.min(g.first*2**Math.max(0,e-1),g.max),"stableBackoffMs");function V(e){const d=new Set;async function p(s,a,o){const t=e.db(),n=e.now(),l=await e.call(s,{method:"POST",path:"/v1/peer/events",body:{events:a.map(r=>JSON.parse(r.payload_json))}}),m=f(r=>{for(const c of a){const h=c.attempts+1,_=c.first_failed_at??n;o&&r?S(t,c.id,h):o?n-_>=E?S(t,c.id,h):x(t,c.id,h,n+Math.min(M(h),E-(n-_)),_):n-_>=G?O(t,c.id):x(t,c.id,h,n+M(h),_)}},"fail");if(A(l)){m(l.unreachable.reason==="machine_offline");return}const i=l.body;if(l.status!==200||i===null||!Array.isArray(i.accepted)){m(!1);return}const y=new Set(i.accepted),b=new Set((Array.isArray(i.rejected)?i.rejected:[]).map(r=>r.key));for(const r of a)y.has(r.idempotency_key)?q(t,r.id):b.has(r.idempotency_key)?O(t,r.id):x(t,r.id,r.attempts+1,n+M(r.attempts+1),r.first_failed_at??n)}f(p,"sendBatch");async function w(s){const a=s.filter(o=>!d.has(o.id));for(const o of a)d.add(o.id);try{const o=new Map;for(const t of a)o.set(t.peer,[...o.get(t.peer)??[],t]);await Promise.all([...o].map(async([t,n])=>{const m=(await e.reach(t).catch(()=>null))?.reach.kind==="relay";for(let i=0;i<n.length;i+=P)await p(t,n.slice(i,i+P),m)}))}finally{for(const o of a)d.delete(o.id)}}f(w,"pushRows");let u=0;return{async run(){const s=e.db(),a=e.now();a-u>=60*6e4&&(u=a,W(s,a),z(s,a)),await w(N(s,a))},flush:f(s=>w(Y(e.db(),s,e.now())),"flush"),async pull(){const s=e.self(),a=(await e.machines()).filter(t=>k(t.machine_id)!==s&&t.reach.kind==="stable");let o=0;for(const t of a){const n=k(t.machine_id);for(let l=0;l<J;l++){const m=await e.call(n,{method:"POST",path:"/v1/peer/outbox/claim",body:{limit:100}}),i=!A(m)&&m.status===200?m.body?.rows:void 0;if(!Array.isArray(i)||i.length===0)break;const y=L(c=>F(c,n,i.map(h=>h.payload)));o+=y.accepted.length;const b=new Set([...y.accepted,...y.rejected.map(c=>c.key)]),r=i.filter(c=>b.has(c.key)).map(c=>({key:c.key,claim_token:c.claim_token}));if(r.length>0&&await e.call(n,{method:"POST",path:"/v1/peer/outbox/ack",body:{acks:r}}),i.length<100)break}}return o}}}f(V,"createOutboxLane");function re(e){if(!e.joined)return()=>{};const d=e.deps??K,p=V(d),w=f(n=>l=>{R({level:"warn",event:`daemon.peer.${n}_failed`,error:l})},"quiet"),u=f((n,l)=>e.register(n().then(()=>{},w(l))),"guarded"),v=X(n=>u(()=>p.flush(n),"outbox_flush")),s=setInterval(()=>u(()=>p.run(),"outbox_run"),e.tickMs??5e3);s.unref();const a=f(()=>u(async()=>{(await d.machines()).find(l=>k(l.machine_id)===d.self())?.reach.kind==="relay"&&await p.pull()},"outbox_pull"),"pull"),o=U.on(a),t=setInterval(a,H);return t.unref(),a(),()=>{v(),o(),clearInterval(s),clearInterval(t)}}f(re,"startPeerOutbox");export{H as PULL_INTERVAL_MS,E as RELAY_RETRY_WINDOW_MS,g as STABLE_BACKOFF_MS,V as createOutboxLane,K as liveOutboxDeps,M as stableBackoffMs,re as startPeerOutbox};
@@ -0,0 +1,35 @@
1
+ import { WebSocket } from 'ws';
2
+ import { type TokenCache } from '../../core/peer/token.js';
3
+ export declare const BACKOFF_MIN_MS = 1000;
4
+ export declare const BACKOFF_MAX_MS = 60000;
5
+ /** A `token` frame goes this long before the current token's `exp`. */
6
+ export declare const TOKEN_REFRESH_LEAD_MS = 60000;
7
+ /** The relay pings every 20 s; a socket that has said nothing for this long is dead. */
8
+ export declare const SILENCE_LIMIT_MS = 60000;
9
+ export declare const HELLO_TIMEOUT_MS = 10000;
10
+ export interface RelayClientOptions {
11
+ /** The directory (`iss` of the machine tokens). */
12
+ issuer: string;
13
+ /** The relay origin from the machine's reach (`https://relay.northlight.ai`); also the token audience. */
14
+ relayOrigin: string;
15
+ /** The unix socket the private ingress listens on. */
16
+ socketPath: string;
17
+ tokens?: TokenCache;
18
+ backoffMinMs?: number;
19
+ backoffMaxMs?: number;
20
+ refreshLeadMs?: number;
21
+ silenceLimitMs?: number;
22
+ helloTimeoutMs?: number;
23
+ /** Test seam: the WebSocket constructor. */
24
+ connect?: (url: string) => WebSocket;
25
+ }
26
+ export interface RelayClient {
27
+ /** True between `hello_ok` and the socket closing. */
28
+ connected(): boolean;
29
+ /** Drop the socket (the machine slept; it is probably dead) and dial again at once. */
30
+ reconnectNow(): void;
31
+ stop(): Promise<void>;
32
+ }
33
+ /** `https://relay.northlight.ai` -> `wss://relay.northlight.ai/v1/connect`. */
34
+ export declare function relayConnectUrl(origin: string): string;
35
+ export declare function startRelayClient(serveOptions: RelayClientOptions): RelayClient;
@@ -0,0 +1 @@
1
+ var I=Object.defineProperty;var o=(_,e)=>I(_,"name",{value:e,configurable:!0});import{request as Y}from"node:http";import{decodeJwt as D}from"jose";import{WebSocket as R}from"ws";import{emitEvent as b}from"../../core/events/emit.js";import{relayConnected as q}from"../../core/peer/events.js";import{peerTokens as j}from"../../core/peer/token.js";import{parseServerFrame as z,RELAY_CLOSE as B,RELAY_MAX_BODY_BYTES as C,RELAY_PROTOCOL_VERSION as H}from"./relay-protocol.js";const J=1e3,K=6e4,U=6e4,X=6e4,V=1e4,W=["authorization","crtr-envelope","crtr-version","content-type"];function G(_){const e=new URL("/v1/connect",_);return e.protocol=e.protocol==="http:"?"ws:":"wss:",e.toString()}o(G,"relayConnectUrl");const Q=o(_=>{const e={host:"peer.local"};for(const E of W){const p=_[E];p!==void 0&&(e[E]=p)}return e},"pick");function se(_){const e={backoffMinMs:J,backoffMaxMs:K,refreshLeadMs:U,silenceLimitMs:X,helloTimeoutMs:V,tokens:j,connect:o(r=>new R(r,{maxPayload:Math.ceil(C*4/3)+65536}),"connect"),..._},E=G(e.relayOrigin);let p=!1,T,O=!1,k=e.backoffMinMs,m,v=0,L=!1;const h=new Map;function A(){if(m=void 0,p)return;const r=++v;(async()=>{let i;try{i=await e.tokens.get(e.issuer,e.relayOrigin)}catch(n){return b({level:"warn",event:"daemon.peer.relay_token_failed",error:n instanceof Error?n:new Error(String(n))}),x(r)}p||r!==v||N(i,r)})()}o(A,"dial");function x(r,i){if(p||r!==v||m!==void 0)return;const n=i??k;k=Math.min(k*2,e.backoffMaxMs),m=setTimeout(A,n),m.unref()}o(x,"schedule");function N(r,i){const n=e.connect(E);T=n;let l,a,f,u=!1,g=!1;const M=o(t=>{n.readyState===R.OPEN&&n.send(JSON.stringify(t))},"send"),d=o(()=>{clearTimeout(f),f=setTimeout(()=>{b({level:"warn",event:"daemon.peer.relay_silent"}),n.terminate()},e.silenceLimitMs),f.unref()},"heard"),w=o((t,c)=>{if(g)return;g=!0,clearTimeout(l),clearTimeout(a),clearTimeout(f);for(const[,S]of h)S.abort();h.clear(),T===n&&(T=void 0),O=!1,u?b({level:"info",event:"daemon.peer.relay_disconnected",fields:{code:t,reason:c}}):b({level:"warn",event:"daemon.peer.relay_connect_failed",fields:{code:t,reason:c}}),t===B.unauthorized&&e.tokens.drop(e.issuer,e.relayOrigin);const s=L;L=!1,x(i,s?0:t===B.replaced?e.backoffMaxMs:void 0)},"finish"),y=o(t=>{clearTimeout(a);const c=Math.max(0,t*1e3-e.refreshLeadMs-Date.now());a=setTimeout(()=>{e.tokens.drop(e.issuer,e.relayOrigin),e.tokens.get(e.issuer,e.relayOrigin).then(s=>{if(g)return;M({type:"token",token:s});const S=D(s).exp;y(typeof S=="number"&&S>t?S:t+1)},s=>{b({level:"warn",event:"daemon.peer.relay_refresh_failed",error:s instanceof Error?s:new Error(String(s))}),g||(clearTimeout(a),a=setTimeout(()=>y(t),5e3),a.unref())})},c),a.unref()},"planRefresh");n.on("open",()=>{M({type:"hello",protocol:H,token:r}),l=setTimeout(()=>n.terminate(),e.helloTimeoutMs),l.unref(),d()}),n.on("message",(t,c)=>{if(d(),c)return;const s=z(Array.isArray(t)?Buffer.concat(t).toString("utf8"):Buffer.from(t).toString("utf8"));s!==null&&F(s)}),n.on("close",(t,c)=>w(t,c.toString("utf8"))),n.on("error",t=>{b({level:"debug",event:"daemon.peer.relay_socket_error",error:t}),w(1006,"error");try{n.terminate()}catch{}});function F(t){switch(t.type){case"hello_ok":{clearTimeout(l),u=!0,O=!0,k=e.backoffMinMs,y(t.expires_at),b({level:"info",event:"daemon.peer.relay_connected",fields:{relay:e.relayOrigin,machine_id:t.machine_id}}),q.emit();return}case"ping":M({type:"pong"});return;case"pong":return;case"cancel":h.get(t.id)?.abort(),h.delete(t.id);return;case"request":{if(!u)return;const c=new AbortController;h.set(t.id,c),P(t,c.signal).then(s=>{h.delete(t.id),c.signal.aborted||M({type:"response",id:t.id,...s})});return}}}o(F,"handle")}o(N,"run");function P(r,i){const n=o((l,a,f)=>({status:l,headers:{"content-type":"application/json"},body_b64:Buffer.from(JSON.stringify({error:{code:a,message:f}})).toString("base64")}),"fail");return new Promise(l=>{const a=Buffer.from(r.body_b64,"base64"),f=Y({socketPath:e.socketPath,method:r.method,path:r.path,headers:{...Q(r.headers),"content-length":String(a.byteLength)},signal:i},u=>{const g=[];let M=0;u.on("data",d=>{if(M+=d.byteLength,M>C){l(n(502,"response_too_large","the answer is over 1 MiB")),f.destroy();return}g.push(d)}),u.on("end",()=>{const d={};for(const[w,y]of Object.entries(u.headers))typeof y=="string"?d[w]=y:Array.isArray(y)&&(d[w]=y.join(", "));l({status:Math.min(599,Math.max(200,u.statusCode??502)),headers:d,body_b64:Buffer.concat(g).toString("base64")})}),u.on("error",()=>l(n(502,"peer_ingress_failed","the peer ingress dropped the answer")))});f.on("error",()=>l(n(502,"peer_ingress_failed","the peer ingress could not be reached"))),f.end(a)})}return o(P,"replay"),A(),{connected:o(()=>O,"connected"),reconnectNow(){if(p)return;clearTimeout(m),m=void 0,k=e.backoffMinMs;const r=T;if(r!==void 0){L=!0,r.terminate();return}A()},async stop(){p=!0,clearTimeout(m),m=void 0,v++;for(const[,i]of h)i.abort();h.clear();const r=T;T=void 0,O=!1,r!==void 0&&await new Promise(i=>{const n=setTimeout(()=>{r.terminate(),i()},1e3);n.unref(),r.once("close",()=>{clearTimeout(n),i()});try{r.close(1e3,"going_away")}catch{r.terminate()}})}}}o(se,"startRelayClient");export{K as BACKOFF_MAX_MS,J as BACKOFF_MIN_MS,V as HELLO_TIMEOUT_MS,X as SILENCE_LIMIT_MS,U as TOKEN_REFRESH_LEAD_MS,G as relayConnectUrl,se as startRelayClient};
@@ -0,0 +1,52 @@
1
+ export declare const RELAY_PROTOCOL_VERSION = 1;
2
+ /** Largest request or response body, in bytes before base64. */
3
+ export declare const RELAY_MAX_BODY_BYTES = 1048576;
4
+ /** WebSocket close codes the relay uses. */
5
+ export declare const RELAY_CLOSE: {
6
+ readonly badFrame: 4000;
7
+ readonly unauthorized: 4001;
8
+ readonly replaced: 4002;
9
+ readonly tokenExpired: 4003;
10
+ };
11
+ export type RelayHeaders = Record<string, string>;
12
+ export type RelayClientFrame = {
13
+ type: 'hello';
14
+ protocol: number;
15
+ token: string;
16
+ } | {
17
+ type: 'token';
18
+ token: string;
19
+ } | {
20
+ type: 'response';
21
+ id: string;
22
+ status: number;
23
+ headers: RelayHeaders;
24
+ body_b64: string;
25
+ } | {
26
+ type: 'ping';
27
+ } | {
28
+ type: 'pong';
29
+ };
30
+ export type RelayServerFrame = {
31
+ type: 'hello_ok';
32
+ protocol: number;
33
+ machine_id: string;
34
+ expires_at: number;
35
+ heartbeat_s: number;
36
+ } | {
37
+ type: 'request';
38
+ id: string;
39
+ method: string;
40
+ path: string;
41
+ headers: RelayHeaders;
42
+ body_b64: string;
43
+ } | {
44
+ type: 'cancel';
45
+ id: string;
46
+ } | {
47
+ type: 'ping';
48
+ } | {
49
+ type: 'pong';
50
+ };
51
+ /** Parse one text frame from the relay; null when it is not a frame this client understands. */
52
+ export declare function parseServerFrame(text: string): RelayServerFrame | null;
@@ -0,0 +1 @@
1
+ var o=Object.defineProperty;var n=(t,e)=>o(t,"name",{value:e,configurable:!0});const c=1,l=1048576,s={badFrame:4e3,unauthorized:4001,replaced:4002,tokenExpired:4003},r=n(t=>typeof t=="string","isString"),p=n(t=>typeof t=="object"&&t!==null&&!Array.isArray(t)&&Object.values(t).every(r),"isHeaders");function u(t){let e;try{e=JSON.parse(t)}catch{return null}if(typeof e!="object"||e===null)return null;switch(e.type){case"hello_ok":return r(e.machine_id)&&typeof e.expires_at=="number"&&typeof e.heartbeat_s=="number"&&typeof e.protocol=="number"?e:null;case"request":return r(e.id)&&r(e.method)&&r(e.path)&&p(e.headers)&&r(e.body_b64)?e:null;case"cancel":return r(e.id)?{type:"cancel",id:e.id}:null;case"ping":return{type:"ping"};case"pong":return{type:"pong"};default:return null}}n(u,"parseServerFrame");export{s as RELAY_CLOSE,l as RELAY_MAX_BODY_BYTES,c as RELAY_PROTOCOL_VERSION,u as parseServerFrame};
@@ -0,0 +1,26 @@
1
+ import { type MachineStatus } from '../../core/peer/directory.js';
2
+ import { type PeerIngressContext } from './ingress.js';
3
+ import { type RelayClientOptions } from './relay-client.js';
4
+ export declare const SYNC_NOTICE_PATH = "/v1/runtime/sync-notice";
5
+ /** How often the clocks are compared to notice that the machine slept. */
6
+ export declare const RESUME_CHECK_MS = 5000;
7
+ export interface PeerRelayDeps {
8
+ syncGrants(options?: {
9
+ fresh?: boolean;
10
+ }): Promise<void>;
11
+ /** Overrides for tests. */
12
+ unsandboxed?: () => boolean;
13
+ issuer?: () => string | null;
14
+ jwksUrl?: () => string | undefined;
15
+ socketPath?: string;
16
+ client?: Partial<RelayClientOptions>;
17
+ ingress?: Partial<PeerIngressContext>;
18
+ resumeCheckMs?: number;
19
+ }
20
+ export interface PeerRelay {
21
+ /** Called after each directory status answer. Starts the relay when it qualifies; restarts it if the relay origin changed. */
22
+ observe(status: MachineStatus | null): Promise<void>;
23
+ running(): boolean;
24
+ stop(): Promise<void>;
25
+ }
26
+ export declare function createPeerRelay(deps: PeerRelayDeps): PeerRelay;
@@ -0,0 +1 @@
1
+ var U=Object.defineProperty;var o=(e,c)=>U(e,"name",{value:c,configurable:!0});import{JwksCache as E}from"@north-light/identity";import{randomUUID as N}from"node:crypto";import{readConfig as k}from"../../core/config.js";import{hasSyncedGrants as O}from"../../core/grants/mirror.js";import{emitEvent as _}from"../../core/events/emit.js";import{directoryIssuer as b,machineUrl as j,ownMachineId as R}from"../../core/peer/directory.js";import{RunError as l}from"../../core/runs/operations.js";import{createResumeDetector as H}from"../resume-detector.js";import{applySyncNotice as M,NoticeLedger as J}from"../api/sync-notice.js";import{runtimeOwner as T,servePeer as D}from"./ingress.js";import{startLocalIngress as G}from"./local-ingress.js";import{startRelayClient as L}from"./relay-client.js";const $="/v1/runtime/sync-notice",z=5e3;async function A(e,c,f){if(e.headersSent){e.destroy();return}e.writeHead(c,{"content-type":"application/json; charset=utf-8"}),e.end(JSON.stringify(f))}o(A,"answerJson");function oe(e){const c=e.unsandboxed??(()=>k("user").runtime?.unsandboxed===!0),f=e.issuer??b;let s;const S=new J;async function m(){const t=s;s=void 0,t!==void 0&&(clearInterval(t.resume),await t.client.stop(),await t.ingress.close())}o(m,"stopStarted");async function C(t,r){const i=new E(r,fetch,e.jwksUrl?e.jwksUrl():k("user").runtime?.jwksUrl),d={issuer:r,jwks:i,audience:j,owner:T,self:R,ready:o(()=>O(r.replace(/\/$/,"")),"ready"),...e.ingress},w=await G(o(async(a,u)=>{const x=(a.url??"").split("?",1)[0];if(a.method==="POST"&&x===$){a.resume();try{const n=d.audience();if(n===null||d.ready?.()===!1)throw new l(503,"runtime_starting","runtime_starting");await M(await S.accept(a.headers.authorization,i,r,n),e.syncGrants,N()),u.writeHead(204),u.end()}catch(n){const I=n instanceof l?n.status:500;await A(u,I,{error:{code:n instanceof l?n.code:"server_failure",message:n instanceof l?n.message:"sync notice failed"}})}return}await D(a,u,d)},"serve"),e.socketPath);let y;try{y=L({issuer:r,relayOrigin:t,socketPath:w.socketPath,...e.client})}catch(a){throw await w.close(),a}const P=H(),v=setInterval(()=>{P.check()&&(_({level:"info",event:"daemon.peer.relay_resume"}),y.reconnectNow())},e.resumeCheckMs??z);v.unref(),s={relay:t,ingress:w,client:y,resume:v,jwks:i},_({level:"info",event:"daemon.peer.relay_started",fields:{relay:t}})}o(C,"begin");let h=Promise.resolve(),p=!1;const g=o(t=>{const r=h.then(t);return h=r.catch(()=>{}),r},"serial");return{observe(t){return g(async()=>{if(p)return;const r=f();if(t===null)return;if(t.reach?.kind!=="relay"||!c()||r===null){await m();return}const i=t.reach.relay.replace(/\/$/,"");s?.relay!==i&&(await m(),await C(i,r))})},running:o(()=>s!==void 0,"running"),stop(){return p=!0,g(m)}}}o(oe,"createPeerRelay");export{z as RESUME_CHECK_MS,$ as SYNC_NOTICE_PATH,oe as createPeerRelay};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.446",
3
+ "version": "0.3.447",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -137,7 +137,7 @@
137
137
  },
138
138
  "license": "UNLICENSED",
139
139
  "dependencies": {
140
- "@north-light/identity": "0.3.446",
140
+ "@north-light/identity": "0.3.447",
141
141
  "@earendil-works/pi-agent-core": "1.1.0",
142
142
  "@earendil-works/pi-ai": "1.1.0",
143
143
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.446",
3
+ "version": "0.3.447",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.446",
3
+ "version": "0.3.447",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/cli",
9
- "version": "0.3.446",
9
+ "version": "0.3.447",
10
10
  "hasInstallScript": true,
11
11
  "license": "UNLICENSED",
12
12
  "workspaces": [
@@ -18,7 +18,7 @@
18
18
  "@earendil-works/pi-ai": "1.1.0",
19
19
  "@earendil-works/pi-coding-agent": "1.1.0",
20
20
  "@earendil-works/pi-tui": "1.1.0",
21
- "@north-light/identity": "0.3.446",
21
+ "@north-light/identity": "0.3.447",
22
22
  "cron-parser": "^5.6.0",
23
23
  "esbuild": "^0.27.7",
24
24
  "jose": "^6.2.12",
@@ -12095,17 +12095,17 @@
12095
12095
  },
12096
12096
  "packages/crouter-api": {
12097
12097
  "name": "@north-light/api",
12098
- "version": "0.3.446",
12098
+ "version": "0.3.447",
12099
12099
  "license": "UNLICENSED"
12100
12100
  },
12101
12101
  "packages/crouter-env-docker": {
12102
12102
  "name": "@north-light/env-docker",
12103
- "version": "0.3.446",
12103
+ "version": "0.3.447",
12104
12104
  "license": "UNLICENSED"
12105
12105
  },
12106
12106
  "packages/crouter-identity": {
12107
12107
  "name": "@north-light/identity",
12108
- "version": "0.3.446",
12108
+ "version": "0.3.447",
12109
12109
  "license": "UNLICENSED",
12110
12110
  "dependencies": {
12111
12111
  "jose": "^6.2.1"
@@ -12113,20 +12113,20 @@
12113
12113
  },
12114
12114
  "packages/crouter-plugin": {
12115
12115
  "name": "@north-light/plugin",
12116
- "version": "0.3.446",
12116
+ "version": "0.3.447",
12117
12117
  "license": "UNLICENSED",
12118
12118
  "dependencies": {
12119
- "@north-light/api": "0.3.446",
12120
- "@north-light/identity": "0.3.446"
12119
+ "@north-light/api": "0.3.447",
12120
+ "@north-light/identity": "0.3.447"
12121
12121
  }
12122
12122
  },
12123
12123
  "packages/crouter-sdk": {
12124
12124
  "name": "@north-light/sdk",
12125
- "version": "0.3.446",
12125
+ "version": "0.3.447",
12126
12126
  "license": "UNLICENSED",
12127
12127
  "dependencies": {
12128
- "@north-light/api": "0.3.446",
12129
- "@north-light/identity": "0.3.446",
12128
+ "@north-light/api": "0.3.447",
12129
+ "@north-light/identity": "0.3.447",
12130
12130
  "jose": "^6.2.1"
12131
12131
  },
12132
12132
  "bin": {