@north-light/cli 0.3.486 → 0.3.487
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/builtin-memory/crouter-sdk/release-notes.md +6 -0
- package/dist/clients/attach/viewer.js +132 -132
- package/dist/core/grants/cover.d.ts +14 -0
- package/dist/core/grants/cover.js +1 -0
- package/dist/core/grants/mirror.d.ts +3 -1
- package/dist/core/grants/mirror.js +2 -2
- package/dist/daemon/api/app-listener.js +13 -13
- package/dist/daemon/api/principal.d.ts +10 -18
- package/dist/daemon/api/principal.js +1 -1
- package/dist/daemon/peer/authority.d.ts +27 -0
- package/dist/daemon/peer/authority.js +1 -0
- package/dist/daemon/peer/ingress.d.ts +17 -15
- package/dist/daemon/peer/ingress.js +1 -1
- package/dist/daemon/peer/relay-service.js +1 -1
- package/docs/sdk/release-notes.md +6 -0
- package/package.json +2 -2
- package/packages/crouter-identity/dist/index.d.ts +55 -0
- package/packages/crouter-identity/dist/index.js +117 -0
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ActiveGrant } from './mirror.js';
|
|
2
|
+
/** Pulls the directory's grants into the mirror; `fresh` demands a pull that starts after the call. */
|
|
3
|
+
export type GrantSync = (options?: {
|
|
4
|
+
fresh?: boolean;
|
|
5
|
+
}) => Promise<void>;
|
|
6
|
+
/** Make the mirror cover grant `grantId` at `rev`, pulling once when it is behind. Throws `RunError` 503
|
|
7
|
+
* `service_unavailable` (failed pull, or a pull that stayed behind) or 401 `grant_revoked` (absent after a pull). */
|
|
8
|
+
export declare function ensureCovers(grantId: string, rev: number, syncGrants: GrantSync): Promise<void>;
|
|
9
|
+
/** The mirrored grant a caller names, read now, when it is still the caller's: active, the same `sub` and
|
|
10
|
+
* `grantee`, and not past `expires_at`. Otherwise 401 `grant_revoked`. */
|
|
11
|
+
export declare function callerGrantOf(grantId: string, sub: string, grantee: string): ActiveGrant;
|
|
12
|
+
/** The grant a token's app link names: the mirror made to cover `rev`, then the grant itself checked against
|
|
13
|
+
* `sub` and `grantee`. Errors are `ensureCovers`' and `callerGrantOf`'s `RunError`s. */
|
|
14
|
+
export declare function coverGrant(grantId: string, rev: number, sub: string, grantee: string, syncGrants: GrantSync): Promise<ActiveGrant>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var l=Object.defineProperty;var r=(e,n)=>l(e,"name",{value:n,configurable:!0});import{emitEvent as w}from"../events/emit.js";import{RunError as a}from"../runs/operations.js";import{activeGrant as h,grantExpired as m,grantSyncState as s,grantsGeneration as g}from"./mirror.js";let c=new Map,f=-1;function u(e){const n=g();return n!==f&&(c=new Map,f=n),c.get(e)}r(u,"behindRev");const v=r((e,n)=>e!==null&&e.rev>=n,"covered");async function p(e,n,o){if(v(s(e),n))return;const t=u(e);if(t===void 0||n>t){try{await o({fresh:!0})}catch{throw new a(503,"service_unavailable","grant sync failed")}const i=s(e);if(v(i,n))return;u(e),c.set(e,Math.max(n,c.get(e)??0)),w({level:"error",event:"daemon.grants.behind_token",fields:{grant:e,token_rev:n,mirror_rev:i?.rev??0}})}throw s(e)?new a(503,"service_unavailable","the directory answered grant sync below the access token's grant revision"):new a(401,"grant_revoked","grant is not active")}r(p,"ensureCovers");function b(e,n,o){const t=h(e);if(!t||t.sub!==n||t.grantee!==o||m(t))throw new a(401,"grant_revoked","grant is not active");return t}r(b,"callerGrantOf");async function x(e,n,o,t,i){return await p(e,n,i),b(e,o,t)}r(x,"coverGrant");export{b as callerGrantOf,x as coverGrant,p as ensureCovers};
|
|
@@ -44,7 +44,9 @@ export declare function mirrorOnly(grant: Pick<GrantRecord, 'grantee'>): boolean
|
|
|
44
44
|
export declare function localGrants(): GrantRecord[];
|
|
45
45
|
/** Every mirrored grant, whatever its status. */
|
|
46
46
|
export declare function allGrants(): GrantRecord[];
|
|
47
|
-
/**
|
|
47
|
+
/** Whether a grant's `expires_at` has passed. An unparsable time reads as expired (fail closed). */
|
|
48
|
+
export declare function grantExpired(grant: Pick<GrantRecord, 'expires_at'>): boolean;
|
|
49
|
+
/** The active form of a grant record read earlier, or null when it is not active, is past `expires_at`, or is unparsable. */
|
|
48
50
|
export declare function activeRecord(grant: GrantRecord): ActiveGrant | null;
|
|
49
51
|
/** `grant.get`'s `version`: an opaque string that changes exactly when what an app
|
|
50
52
|
* reads from its grant changes — `rev`, `resolved_scopes` (order kept), `spend_cap`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var _=Object.defineProperty;var t=(e,r)=>_(e,"name",{value:r,configurable:!0});import{createHash as y}from"node:crypto";import{prepared as s}from"../sqlite-statements.js";import{parseScope as S}from"@north-light/identity";import{openDb as i,withCanvasWrite as v}from"../canvas/db.js";import{isUnsandboxedMachine as T}from"../runtime/unsandboxed.js";const m=["not_started","pending","ready","failed","unavailable"];function c(e){if(!Array.isArray(e))throw new TypeError("grant scopes must be a list");return e.map(r=>{if(!r||typeof r!="object"||typeof r.scope!="string"||!["always","ask"].includes(r.mode))throw new TypeError("invalid grant scope entry");return{scope:r.scope,mode:r.mode}})}t(c,"entries");function O(e){if(!e||typeof e!="object"||Array.isArray(e))return null;const{state:r,message:a,next:u,step:n}=e;return typeof r!="string"||!m.includes(r)||a!==void 0&&typeof a!="string"||n!==void 0&&typeof n!="string"||typeof u!="string"||!u?null:{state:r,...a===void 0?{}:{message:a},next:u,...n===void 0?{}:{step:n}}}t(O,"dependencyOf");function E(e){if(!e||typeof e!="object"||Array.isArray(e))return{};const r={};for(const[a,u]of Object.entries(e)){const n=O(u);n&&(r[a]=n)}return r}t(E,"dependenciesOf");function l(e){return{id:e.id,runtime:e.runtime,sub:e.sub,grantee:e.grantee,rev:e.rev,scopes:c(JSON.parse(e.scopes_json)),resolved_scopes:c(JSON.parse(e.resolved_scopes_json)),spend_cap:e.spend_cap,expires_at:e.expires_at,status:e.status,revoked_at:e.revoked_at,approved_at:e.approved_at,payer:e.payer,dependencies:E(JSON.parse(e.dependencies_json)),run_notice_url:e.run_notice_url??null,synced_at:e.synced_at}}t(l,"record");function p(e){if(!e||e.status!=="active")return null;try{return N(l(e))}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(p,"active");function d(e){return e.grantee!=="app:terminal"&&T()}t(d,"mirrorOnly");function b(){return R().filter(e=>!d(e))}t(b,"localGrants");function R(){return s(i(),"SELECT * FROM grants").all().flatMap(e=>{try{return[l(e)]}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return[];throw r}})}t(R,"allGrants");function x(e){return e.expires_at!==null&&!(Date.parse(e.expires_at)>Date.now())}t(x,"grantExpired");function N(e){if(e.status!=="active"||x(e))return null;try{const r=e.resolved_scopes.map(({scope:a})=>S(a));return{...e,resolved:e.resolved_scopes.flatMap(({mode:a},u)=>a==="always"?[r[u]]:[])}}catch(r){if(r instanceof TypeError||r instanceof SyntaxError)return null;throw r}}t(N,"activeRecord");function f(e){return Array.isArray(e)?`[${e.map(f).join(",")}]`:e&&typeof e=="object"?`{${Object.keys(e).sort().filter(r=>e[r]!==void 0).map(r=>`${JSON.stringify(r)}:${f(e[r])}`).join(",")}}`:JSON.stringify(e)??"null"}t(f,"canonicalJson");function k(e){const r={rev:e.rev,resolved_scopes:e.resolved_scopes,spend_cap:e.spend_cap,payer:e.payer,dependencies:e.dependencies??{}};return y("sha256").update(f(r)).digest("base64url").slice(0,22)}t(k,"grantVersion");function D(e){return!!s(i(),"SELECT 1 FROM grants WHERE id = ?").get(e)}t(D,"hasGrant");function H(e){return s(i(),"SELECT rev, status FROM grants WHERE id = ?").get(e)??null}t(H,"grantSyncState");function W(e,r){return s(i(),"SELECT rev, status FROM grants WHERE grantee = ? AND sub = ?").get(e,r)??null}t(W,"grantSyncStateFor");function j(e){const r=s(i(),"SELECT * FROM grants WHERE id = ?").get(e);if(!r)return null;try{return l(r)}catch(a){if(a instanceof TypeError||a instanceof SyntaxError)return null;throw a}}t(j,"grantRecord");function G(e){return p(s(i(),"SELECT * FROM grants WHERE id = ?").get(e))}t(G,"activeGrant");function I(e,r){return p(s(i(),"SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(e,r))}t(I,"grantFor");function J(e){if(d({grantee:e}))return null;const r=s(i(),"SELECT * FROM grants WHERE grantee = ? AND status = 'active'").all(e);if(r.length>1)throw new Error(`multiple active grants for ${e}`);return p(r[0])}t(J,"activeGrantForApp");function U(e){const r=s(i(),"SELECT run_notice_url AS url, sub, grantee FROM grants WHERE id = ? AND status = 'active'").get(e);return r&&r.url&&!d(r)?{url:r.url,sub:r.sub,grantee:r.grantee}:null}t(U,"runNoticeTarget");function w(){const e=s(i(),"SELECT * FROM grants WHERE grantee = 'app:terminal' AND status = 'active'").all();if(e.length>1)throw new Error("multiple active terminal grants");return p(e[0])}t(w,"terminalGrant");function $(){return s(i(),"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value??null}t($,"grantsSource");function V(e){return s(i(),"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value===e}t(V,"hasSyncedGrants");function P(e){return s(e,"SELECT value FROM runtime_state WHERE key = 'grants_source'").get()?.value!=="local"?0:(s(e,"DELETE FROM runtime_state WHERE key = 'grants_source'").run(),Number(s(e,"DELETE FROM grants").run().changes))}t(P,"dropLocalGrantMirror");function X(e,r,a){const u=new Set;for(const n of e){if(u.has(n.id)||!n.id||!n.runtime||!n.sub||!n.grantee||!Number.isInteger(n.rev)||!["active","revoked","removed"].includes(n.status))throw new TypeError("invalid grant sync response");u.add(n.id),c(n.scopes),c(n.resolved_scopes)}v(n=>{n.exec("DELETE FROM grants");const g=s(n,`INSERT INTO grants
|
|
2
2
|
(id, runtime, sub, grantee, rev, scopes_json, resolved_scopes_json, spend_cap, expires_at,
|
|
3
3
|
status, revoked_at, approved_at, payer, dependencies_json, run_notice_url, synced_at)
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const o of e)g.run(o.id,o.runtime,o.sub,o.grantee,o.rev,JSON.stringify(o.scopes),JSON.stringify(o.resolved_scopes),o.spend_cap,o.expires_at,o.status,o.revoked_at,o.approved_at,o.payer,JSON.stringify(E(o.dependencies)),o.run_notice_url??null,o.synced_at);s(n,"INSERT INTO runtime_state (key, value) VALUES ('grants_source', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(a),s(n,"INSERT INTO runtime_state (key, value) VALUES ('grants_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)").run()})}t(
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);for(const o of e)g.run(o.id,o.runtime,o.sub,o.grantee,o.rev,JSON.stringify(o.scopes),JSON.stringify(o.resolved_scopes),o.spend_cap,o.expires_at,o.status,o.revoked_at,o.approved_at,o.payer,JSON.stringify(E(o.dependencies)),o.run_notice_url??null,o.synced_at);s(n,"INSERT INTO runtime_state (key, value) VALUES ('grants_source', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(a),s(n,"INSERT INTO runtime_state (key, value) VALUES ('grants_generation', '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)").run()})}t(X,"replaceGrants");function q(){const e=s(i(),"SELECT value FROM runtime_state WHERE key = 'grants_generation'").get();return e?Number(e.value):0}t(q,"grantsGeneration");export{G as activeGrant,J as activeGrantForApp,N as activeRecord,R as allGrants,E as dependenciesOf,P as dropLocalGrantMirror,x as grantExpired,I as grantFor,j as grantRecord,H as grantSyncState,W as grantSyncStateFor,k as grantVersion,q as grantsGeneration,$ as grantsSource,D as hasGrant,V as hasSyncedGrants,b as localGrants,d as mirrorOnly,X as replaceGrants,U as runNoticeTarget,w as terminalGrant};
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
var
|
|
2
|
-
data: ${JSON.stringify(
|
|
1
|
+
var z=Object.defineProperty;var s=(e,r)=>z(e,"name",{value:r,configurable:!0});import{randomUUID as F}from"node:crypto";import{createServer as G}from"node:http";import{Router as J}from"./router.js";import{toErrorBody as Y}from"./map.js";import{objectRoutes as q}from"./handlers/objects.js";import{customObjectRoutes as Q}from"./handlers/custom-objects.js";import{docRoutes as W}from"./handlers/docs.js";import{OPERATIONS as K}from"./operations.js";import{errorCodes as L}from"../../api/error-codes.js";import{MINIMUM_RUNTIME_VERSION_HEADER as Z,RUNTIME_VERSION_HEADER as ee,minimumRuntimeRefusal as te}from"../../api/runtime-version.js";import{CONTRACT_HEADER as ne,CONTRACT_LEVEL as U,REQUIRED_CONTRACT_HEADER as re,contractRefusal as ae}from"../../api/contract.js";import{crtrVersion as x}from"../../shared/crtr-version.js";import{AppPrincipalResolver as oe}from"./principal.js";import{isDaemonRoute as se,runtimeOwner as ie,servePeer as V}from"../peer/ingress.js";import{applySyncNotice as ue}from"./sync-notice.js";import{ownMachineId as de}from"../../core/peer/directory.js";import{handleRunOperation as ce,integer as le}from"./handlers/runs.js";import{parseLlmRequest as fe,runLlm as me}from"../../core/llm/run.js";import{parseUsageMonth as pe,usageFor as he}from"../../core/usage/ledger.js";import{capFields as we}from"./handlers/llm.js";import{CONTROL_BODY_MAX_BYTES as ye,MODEL_CONTEXT_BODY_MAX_BYTES as _e}from"../../core/llm/body-limits.js";import{RunError as m,runIsRunning as ve}from"../../core/runs/operations.js";import{requireRun as j}from"./router.js";import{deferHead as ge}from"./deferred-head.js";import{readRunEvents as X,registerGrantStream as Re,subscribeRunEvents as Oe}from"../../core/runs/events.js";import{activeGrant as Ee}from"../../core/grants/mirror.js";import{releaseGrantWaiters as be}from"../../core/grants/watch.js";import{runNotices as Se}from"../run-notices.js";import{handleVendorSignIn as Te,isVendorSignInPath as Ae}from"./vendor-signin.js";import{listAppProfiles as Ne}from"./handlers/app-profiles.js";import{handleTransferDownload as Ce,handleTransferOperation as Ie,TRANSFER_OPERATIONS as Pe}from"./handlers/transfer.js";import{handleQuestionOperation as ke,QUESTION_OPERATIONS as He}from"./handlers/questions.js";import{vendorStatus as Me}from"./handlers/vendor-status.js";import{handleMemoryOperation as De,MEMORY_OPERATIONS as xe}from"./handlers/memory.js";import{abortOnClose as je,callerFromApp as Be,handleProviderOperation as Je,PROVIDER_OPERATIONS as Le,providerFailure as Ue}from"./handlers/providers.js";function Ve(e){return Object.prototype.hasOwnProperty.call(L,e)?L[e]:void 0}s(Ve,"fixedRow");function Xe(e){const r=Ve(e);return r?r.type:e==="stream_gap"||e==="provider_error"||e==="runtime_version_unsupported"?"invalid_request_error":"server_error"}s(Xe,"errorType");function $e(e){if(!e||typeof e!="object"||Array.isArray(e)||!("rate_limit"in e))return e;const{rate_limit:r,...o}=e;return o}s($e,"appDetails");function D(e,r,o){const c=$e(e.details);return{error:{code:e.code,message:e.message,...c===void 0?{}:{details:c},type:Xe(e.code),...e.param?{param:e.param}:{},origin:"runtime",retryable:e.retryable||e.code==="stream_dropped",request_id:r,...o?{run_id:o}:{},...e.scope?{scope:e.scope}:{},...e.code==="unauthorized"?{user_action:"reconnect"}:{},...we(e),...e.code==="all_routes_exhausted"?{retry_after_s:e.details.retry_after_s}:{}}}}s(D,"envelope");function ze(e,r,o,c){const u=r instanceof m?r:new m(500,"server_failure","runtime request failed");if(r instanceof m||console.error("[app-listener]",r),e.headersSent&&!String(e.getHeader("Content-Type")??"").startsWith("text/event-stream")){e.destroy();return}if(e.headersSent){e.end(`event: error
|
|
2
|
+
data: ${JSON.stringify(D(u,o,c))}
|
|
3
3
|
|
|
4
|
-
`);return}e.writeHead(
|
|
4
|
+
`);return}e.writeHead(u.status,{"Content-Type":"application/json; charset=utf-8","Request-Id":o,"X-Content-Type-Options":"nosniff",...u.code==="all_routes_exhausted"?{"Retry-After":String(u.details.retry_after_s)}:{}}),e.end(JSON.stringify(D(u,o,c)))}s(ze,"fail");function $(e){return Array.isArray(e)?e[0]:e}s($,"headerValue");function O(e,r,o){e.writeHead(r,{"Content-Type":"application/json; charset=utf-8"}),e.end(JSON.stringify(o))}s(O,"json");async function I(e,r=ye){const o=[];let c=0;for await(const u of e){if(c+=u.length,c>r)throw new m(400,"invalid_request","request body too large");o.push(u)}if(c)try{return JSON.parse(Buffer.concat(o).toString("utf8"))}catch{throw new m(400,"invalid_request","invalid JSON body")}}s(I,"bodyOf");function Fe(e,r){const o=e.headers["last-event-id"]??r.get("after");if(Array.isArray(o))throw new m(400,"invalid_request","invalid cursor","after");return le(o??null,"after",0,Number.MAX_SAFE_INTEGER,0)}s(Fe,"cursor");function Ge(e){return`id: ${e.sequence_number}
|
|
5
5
|
event: ${e.type}
|
|
6
6
|
data: ${JSON.stringify(e)}
|
|
7
7
|
|
|
8
|
-
`}s(
|
|
8
|
+
`}s(Ge,"frame");const Ye=6e4;function qe(e,r,o,c,u,_,E,A={}){j(c,o);let v=X(o,u);if(u>v.latest_sequence)throw new m(400,"invalid_request","cursor is beyond the run","after");if(u<v.latest_sequence&&(v.earliest_sequence===null||u<v.earliest_sequence-1))throw new m(409,"stream_gap","events have expired",void 0,{earliest_sequence:v.earliest_sequence,latest_sequence:v.latest_sequence});if(r.destroyed||e.destroyed)return r.destroy(),Promise.resolve();r.writeHead(200,{"Content-Type":E==="sse"?"text/event-stream":"application/x-ndjson","Cache-Control":"no-cache","X-Accel-Buffering":"no"}),E==="sse"?r.write(`: connected
|
|
9
9
|
|
|
10
|
-
`):
|
|
11
|
-
data: ${JSON.stringify(
|
|
10
|
+
`):r.flushHeaders();let w=u;const B=Se().streamOpened(o);let n=!1,t=!1,h=!1,b;const N=new Promise(f=>{b=f}),y=s(()=>{n||(n=!0,B(w),clearInterval(l),clearInterval(R),P(),H(),r.off("close",y),r.off("drain",p),r.end(),b())},"end"),p=s(()=>{h=!1,a()},"onDrain"),g=s(f=>{n||r.write(E==="sse"?`event: error
|
|
11
|
+
data: ${JSON.stringify(D(f,_,o))}
|
|
12
12
|
|
|
13
|
-
`:`${JSON.stringify({type:"error",...
|
|
14
|
-
`),y()},"error"),a=s(()=>{if(!(
|
|
15
|
-
`),w=
|
|
13
|
+
`:`${JSON.stringify({type:"error",...D(f,_,o)})}
|
|
14
|
+
`),y()},"error"),a=s(()=>{if(!(n||t)){t=!0;try{if(!T()){k();return}if(j(c,o),v=X(o,w),v.backlog_count>1e3||v.backlog_bytes>32*1024*1024){g(new m(503,"stream_dropped","stream reader fell behind",void 0,{last_sequence:w}));return}for(const f of v.events){if(h)break;h=!r.write(E==="sse"?Ge(f):`${JSON.stringify(f)}
|
|
15
|
+
`),w=f.sequence_number}}catch(f){g(f instanceof m?f:new m(500,"server_failure","stream failed"))}finally{t=!1}}},"flush"),k=s(()=>g(new m(401,"grant_revoked","grant is not active")),"revoked"),T=s(()=>"class"in c||Ee(c.grant)!==null,"grantActive"),P=Oe(o,a),H="class"in c?()=>{}:Re(c.grant,k),l=setInterval(()=>{!n&&!h&&E==="sse"&&r.write(`: keepalive
|
|
16
16
|
|
|
17
|
-
`)},15e3),
|
|
17
|
+
`)},15e3),M=A.idleMs??Ye,i=A.isRunning??ve;let S=Date.now(),d=w;const R=setInterval(()=>{if(n||E!=="sse")return;const f=Date.now();let C=!0;try{C=i(o)}catch{}if(C||w!==d){S=f,d=w;return}f-S<M||h||(r.write(`event: idle
|
|
18
18
|
data: {}
|
|
19
19
|
|
|
20
|
-
`),y())},A.idleCheckMs??1e3);return
|
|
20
|
+
`),y())},A.idleCheckMs??1e3);return R.unref(),r.on("drain",p),r.on("close",y),a(),N}s(qe,"streamRunEvents");function kt(e,r,o,c={}){const u=e.listener;if(!e.issuer||!e.audience||!u?.address||!Number.isInteger(u.port)||u.port<1||u.port>65535)throw new Error("runtime issuer, audience and listener address/port are required");if(["0.0.0.0","::","[::]"].includes(u.address))throw new Error("runtime listener must bind a specific address");const _=new oe(e.issuer,e.audience,o,e.jwksUrl),E=new J,A=new Map;for(const n of K.filter(t=>t.stage!==void 0)){const t=s(()=>({status:500}),"handler");E.register({method:n.method,pattern:n.path,handler:t}),A.set(t,n)}const v=new J().registerAll(q).registerAll(Q).registerAll(W),w=G((n,t)=>{(async()=>{ge(n,t,c.deferHead);const h=F();t.setHeader("Request-Id",h),t.setHeader(ee,x()),t.setHeader(ne,U);let b;try{const N=$(n.headers[Z.toLowerCase()]),y=te(N,x())??ae($(n.headers[re.toLowerCase()]),U,{legacyMinimum:N,runtimeVersion:x()});if(y!==null)throw y.status===400?new m(400,y.code,y.message,y.param):new m(426,y.code,y.message,void 0,y.details);const p=new URL(n.url??"/","http://runtime.local");if(Ae(p.pathname)){await Te(n,t,p,_);return}const g=E.match(n.method??"GET",p.pathname),a=g?A.get(g.handler):void 0,k=a?.name==="runtime.sync-notice"||a?.name==="runtime.vendor-status";let T,P;if(!k&&n.headers.authorization!==void 0)try{T=await _.verifyCall(n.headers.authorization)}catch(i){P=i}const H={issuer:e.issuer,jwks:_.jwks,audience:s(()=>e.audience,"audience"),owner:ie,self:de,syncGrants:o,ready:s(()=>_.isReady()&&r(),"ready")};if(T?.chain[0]?.sub.startsWith("runtime:")){await V(n,t,H,T);return}if(T===void 0&&a===void 0&&await se(n.method??"GET",p.pathname)){await V(n,t,H,{status:401,code:"unauthorized",message:P===void 0?"a machine token is required":"invalid machine token"});return}if((n.method!=="GET"||p.pathname!=="/v1/health")&&(!_.isReady()||!r()))throw new m(503,"runtime_starting","runtime_starting");if(!g||!a)throw new m(404,"not_found","unknown operation");if(b=g.params.run_id,a.name==="health"){O(t,200,{status:"ok",api_version:"2026-09-23"});return}if(a.name==="runtime.sync-notice"){await ue(await _.verifySyncNotice(n.headers.authorization),o,h),t.writeHead(204),t.end();return}if(a.name==="runtime.vendor-status"){await _.verifyDirectoryRead(n.headers.authorization,"vendor_status"),O(t,200,Me());return}if(T===void 0)throw P??new m(401,"unauthorized","a directory access token is required");const l=await _.resolveVerified(T,a.scope);if(t.setHeader("Request-Id",h),b&&j(l,b,["run.message","run.interrupt","run.cancel","run.rename","run.delete"].includes(a.name),a.name==="run.delete"),a.stage==="objects"){const i=v.match(a.method,p.pathname);if(!i)throw new m(404,"not_found","unknown operation");const S={method:a.method,path:p.pathname,params:i.params,query:p.searchParams,body:await I(n,i.maxBodyBytes),ceiling:l.resolved,principal:{class:"app",app:l.grantee,scopes:l.resolved},req:n,res:t};let d;try{d=await i.handler(S)}catch(R){const{status:f,body:C}=Y(R);throw f>=500&&console.error("[app-listener]",R),new m(f,C.error.code,C.error.message,void 0,C.error.details)}if(t.headersSent)return;if(d.rawBody!==void 0||d.status===204||d.body===void 0){t.writeHead(d.status,d.headers??{}),t.end(d.rawBody);return}t.writeHead(d.status,{"Content-Type":"application/json; charset=utf-8",...d.headers??{}}),t.end(JSON.stringify(d.body));return}if(a.stream==="bytes"){await Ce(n,t,l,p.searchParams);return}if(a.stream){qe(n,t,b,l,Fe(n,p.searchParams),h,"sse",c.stream);return}if(a.name==="llm"){const i=fe(await I(n,_e)),S=new AbortController,d=s(()=>S.abort(),"abort");t.on("close",d);try{const R=await me(i,S.signal,i.stream?f=>{t.headersSent||t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache","X-Accel-Buffering":"no"}),t.write(`data: ${JSON.stringify(f)}
|
|
21
21
|
|
|
22
|
-
`)}:void 0,void 0,{grantee:
|
|
22
|
+
`)}:void 0,void 0,{grantee:l.grantee,spend_cap:l.spend_cap});i.stream?(t.headersSent||t.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache","X-Accel-Buffering":"no"}),t.end(`data: [DONE]
|
|
23
23
|
|
|
24
|
-
`)):O(t,200,R.completion)}finally{t.off("close",d)}return}if(a.name==="usage.get"){O(t,200,he(
|
|
24
|
+
`)):O(t,200,R.completion)}finally{t.off("close",d)}return}if(a.name==="usage.get"){O(t,200,he(l.grantee,l.spend_cap,pe(p.searchParams.get("month"))));return}if(a.name==="profile.list"){O(t,200,Ne(l));return}if(He.has(a.name)){const i=await ke(a.name,l,g.params.question_id,a.method==="POST"?await I(n):void 0);O(t,i.status,i.body);return}if(Le.has(a.name)){const{signal:i,release:S}=je(t);try{const d=await Je(a.name,Be(l),{params:g.params,body:a.method==="POST"?await I(n):void 0,query:p.searchParams},{requestId:h,signal:i});O(t,d.status,d.body)}catch(d){const R=Ue(d,h,!0);t.writeHead(R.status,{"Content-Type":"application/json; charset=utf-8","X-Content-Type-Options":"nosniff",...R.headers}),t.end(JSON.stringify(R.body))}finally{S()}return}if(xe.has(a.name)){const i=De(a.name,{class:"app",app:l.grantee,scopes:l.resolved},p.searchParams);O(t,i.status,i.body);return}if(Pe.has(a.name)){const i=await Ie(a,l,g.params,p.searchParams,await I(n));O(t,i.status,i.body);return}const M=await ce(a,l,b,p.searchParams,await I(n),n.headers["idempotency-key"]);O(t,M.status,M.body)}catch(N){ze(t,N,h,b)}})()});return{ready:new Promise((n,t)=>{w.once("error",t),w.listen(u.port,u.address,()=>{w.off("error",t),n()})}),close:s(async()=>{_.close(),be(),w.closeAllConnections(),await new Promise((n,t)=>w.close(h=>h?t(h):n()))},"close")}}s(kt,"createAppListener");export{Ye as RUN_STREAM_IDLE_MS,kt as createAppListener,Fe as cursor,qe as streamRunEvents};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { JwksCache, type Scope } from '@north-light/identity';
|
|
2
|
-
import {
|
|
1
|
+
import { JwksCache, type CallToken, type Scope } from '@north-light/identity';
|
|
2
|
+
import type { ActiveGrant, GrantDependencies, GrantScope } from '../../core/grants/mirror.js';
|
|
3
3
|
import { type NoticeTicket } from './sync-notice.js';
|
|
4
4
|
export interface AppPrincipal {
|
|
5
5
|
sub: string;
|
|
@@ -48,23 +48,15 @@ export declare class AppPrincipalResolver {
|
|
|
48
48
|
* the one this route accepts. It names no owner and is not single-use, so
|
|
49
49
|
* there is no `sub` or `jti` requirement. Every failure is 401. */
|
|
50
50
|
verifyDirectoryRead(authorization: string | undefined, purpose: string): Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
private behind;
|
|
56
|
-
private behindGeneration;
|
|
57
|
-
private behindRev;
|
|
58
|
-
/** The ordering rule (contract 1 "Grant sync"): the mirror covers a token
|
|
59
|
-
* stamped `rev = N` when it holds the grant at `rev >= N`, and the decision
|
|
60
|
-
* is then the mirror's alone. Otherwise one fresh pull — one that starts
|
|
61
|
-
* after this call — is awaited first. A failed pull answers 503; a
|
|
62
|
-
* successful pull that still does not cover the token is a directory fault:
|
|
63
|
-
* an absent grant is refused `grant_revoked`, a grant below the token's rev
|
|
64
|
-
* answers 503, and repeats of either answer without pulling. No clock or
|
|
65
|
-
* timer enters the decision. */
|
|
66
|
-
private ensureCovers;
|
|
51
|
+
/** Verify a bearer once as a call token: ES256 signature against the JWKS mirror and every check of
|
|
52
|
+
* `verifyCallToken` (any shape: new, superset or legacy). The caller routes on `chain[0]`. Authorizes nothing.
|
|
53
|
+
* 503 before the mirror loads; every other failure is 401. */
|
|
54
|
+
verifyCall(authorization: string | undefined): Promise<CallToken>;
|
|
67
55
|
resolve(authorization: string | undefined, scope?: string): Promise<AppPrincipal>;
|
|
56
|
+
/** The caller an already-verified app token names. Grantee, grant and rev come from the token's app link
|
|
57
|
+
* (`chain[0]`, which must be the whole chain); the mirror is made to cover the link's `rev` and the grant
|
|
58
|
+
* is read now (`coverGrant`). A token whose presenter is not an app link holding a grant is refused 401. */
|
|
59
|
+
resolveVerified(token: CallToken, scope?: string): Promise<AppPrincipal>;
|
|
68
60
|
}
|
|
69
61
|
/** The mirrored grant a caller's token names, read now, when it is still the
|
|
70
62
|
* caller's: active, the same `sub` and `grantee`, and not past `expires_at`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var g=Object.defineProperty;var d=(c,e)=>g(c,"name",{value:e,configurable:!0});import{covers as v,JwksCache as m,MissingRevClaimError as p,verifyCallToken as _}from"@north-light/identity";import{createLocalJWKSet as l,decodeProtectedHeader as h,jwtVerify as f}from"jose";import{callerGrantOf as b,coverGrant as k}from"../../core/grants/cover.js";import{RunError as n}from"../../core/runs/operations.js";import{NoticeLedger as E}from"./sync-notice.js";class W{static{d(this,"AppPrincipalResolver")}issuer;audience;syncGrants;cache;ready=!1;timer;constructor(e,t,r,i){this.issuer=e,this.audience=t,this.syncGrants=r,this.cache=new m(e,fetch,i),this.load(),this.timer=setInterval(()=>{this.ready||this.load()},5e3),this.timer.unref()}async load(){try{await this.cache.load(),this.ready=!0}catch{}}isReady(){return this.ready}get jwks(){return this.cache}close(){this.timer&&clearInterval(this.timer)}ledger=new E;async verifySyncNotice(e){if(!this.ready)throw new n(503,"runtime_starting","runtime_starting");return this.ledger.accept(e,this.cache,this.issuer,this.audience)}async verifyHandoff(e){if(!this.ready)throw new n(503,"runtime_starting","runtime_starting");const t=d(()=>new n(401,"unauthorized","invalid vendor sign-in handoff"),"refuse");if(!e)throw t();let r;try{const u=h(e);if(u.alg!=="ES256")throw t();const y=await this.cache.forKid(u.kid);({payload:r}=await f(e,l(y),{issuer:this.issuer,audience:this.audience,algorithms:["ES256"],requiredClaims:["exp","jti","sub"]}))}catch{throw t()}const{sub:i,jti:s,exp:a,purpose:o,aud:w}=r;if(o!=="vendor_sign_in"||w!==this.audience||typeof i!="string"||typeof s!="string"||s===""||typeof a!="number")throw t();return{sub:i,jti:s,exp:a}}async verifyDirectoryRead(e,t){if(!this.ready)throw new n(503,"runtime_starting","runtime_starting");const r=d(()=>new n(401,"unauthorized","invalid directory token"),"refuse");if(!e?.startsWith("Bearer ")||e.length<=7)throw r();const i=e.slice(7);let s;try{const a=h(i);if(a.alg!=="ES256")throw r();const o=await this.cache.forKid(a.kid);({payload:s}=await f(i,l(o),{issuer:this.issuer,audience:this.audience,algorithms:["ES256"],requiredClaims:["exp"]}))}catch{throw r()}if(s.purpose!==t||s.aud!==this.audience)throw r()}async verifyCall(e){if(!this.ready)throw new n(503,"runtime_starting","runtime_starting");if(!e?.startsWith("Bearer ")||e.length<=7)throw new n(401,"unauthorized","a directory access token is required");const t=e.slice(7);try{if(h(t).alg!=="ES256")throw new Error("Invalid call token header");return await _({token:t,jwks:this.cache,issuer:this.issuer,audience:this.audience})}catch(r){throw r instanceof p?new n(401,"unauthorized",r.message):new n(401,"unauthorized","invalid directory access token")}}async resolve(e,t){return this.resolveVerified(await this.verifyCall(e),t)}async resolveVerified(e,t){const r=e.chain[0],i=e.claims;if(e.chain.length!==1||!r||!r.sub.startsWith("app:")||r.grant===void 0||r.rev===void 0||!/^[A-Za-z0-9_-]{1,64}$/.test(i.client_id)||i.grantee!==void 0&&i.grantee!==r.sub||i.rev!==void 0&&i.rev!==r.rev)throw new n(401,"unauthorized","invalid directory access token");const s=await k(r.grant,r.rev,e.sub,r.sub,this.syncGrants);if(t&&!s.resolved.some(a=>v(a,t)))throw new n(403,"scope_missing",`missing scope ${t}`,void 0,void 0,t);return{sub:e.sub,grantee:r.sub,client_id:i.client_id,grant:r.grant,rev:s.rev,resolved:s.resolved,resolved_scopes:s.resolved_scopes,spend_cap:s.spend_cap,dependencies:s.dependencies??{},exp:i.exp}}}function q(c){return b(c.grant,c.sub,c.grantee)}d(q,"callerGrant");export{W as AppPrincipalResolver,q as callerGrant};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CallLink, CallToken } from '@north-light/identity';
|
|
2
|
+
import { type GrantSync } from '../../core/grants/cover.js';
|
|
3
|
+
import { type ActiveGrant } from '../../core/grants/mirror.js';
|
|
4
|
+
import type { ForwardedPrincipal, MachinePrincipal } from '../api/router.js';
|
|
5
|
+
/** What a person may do on this runtime. Shares will add `'watch' | 'message' | 'spawn'`; only `owner` grants the person's authority. */
|
|
6
|
+
export type Relation = 'owner';
|
|
7
|
+
export interface AuthorityDeps {
|
|
8
|
+
/** This machine (`runtime:<id>`). */
|
|
9
|
+
self(): string;
|
|
10
|
+
/** The relation `sub` (`user:<id>`) holds to this runtime: ownership is `sub` = the runtime's owner; the share lookup is stubbed to null. */
|
|
11
|
+
relation(sub: string): Relation | null;
|
|
12
|
+
/** The mirrored grant an app link names, covered to the link's revision and checked against the token's `sub`. Throws `RunError` 403 `app_not_granted` or 503 `service_unavailable`. */
|
|
13
|
+
grantLink(link: CallLink, sub: string): Promise<ActiveGrant>;
|
|
14
|
+
}
|
|
15
|
+
export type PeerRefusal = {
|
|
16
|
+
status: number;
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
export type PeerAuthority = {
|
|
21
|
+
principal: MachinePrincipal | ForwardedPrincipal;
|
|
22
|
+
} | PeerRefusal;
|
|
23
|
+
/** `deps.grantLink` over the mirror: a link carrying `grant` reads that grant (pulling grants when the mirror is below `link.rev`), a link
|
|
24
|
+
* with none (an app named only in the envelope) reads the app's grant for `sub`. Missing, revoked, foreign or expired is 403 `app_not_granted`. */
|
|
25
|
+
export declare function mirrorGrantLink(syncGrants: GrantSync): AuthorityDeps['grantLink'];
|
|
26
|
+
/** The principal a verified call token plus its envelope yields, or the refusal. Authorizes nothing outside the eight rules in order. */
|
|
27
|
+
export declare function peerAuthority(token: CallToken, envelopeHeader: string | undefined, deps: AuthorityDeps): Promise<PeerAuthority>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var b=Object.defineProperty;var c=(e,n)=>b(e,"name",{value:n,configurable:!0});import{decodeEnvelope as m,EnvelopeError as v}from"../../core/peer/envelope.js";import{coverGrant as w}from"../../core/grants/cover.js";import{grantFor as g}from"../../core/grants/mirror.js";import{RunError as f}from"../../core/runs/operations.js";const r=c((e,n,o)=>({status:e,code:n,message:o}),"refuse"),l="the envelope does not match its token";function G(e){return async(n,o)=>{const t=c(()=>new f(403,"app_not_granted",`${n.sub} holds no active grant for ${o} on this machine`),"notGranted");if(n.grant===void 0){const i=g(n.sub,o);if(i===null)throw t();return i}try{return await w(n.grant,n.rev??1,o,n.sub,e)}catch(i){throw i instanceof f&&i.status===401?t():i}}}c(G,"mirrorGrantLink");async function L(e,n,o){const t=e.chain[0];if(t===void 0||!t.sub.startsWith("runtime:"))return r(401,"unauthorized","not a machine's token");if(t.sub===o.self())return r(401,"unauthorized","a machine does not call itself");if(e.scope!==void 0)return r(401,"unauthorized","a provider token does not authorize on a machine");const i=o.relation(e.sub);if(i===null)return r(403,"forbidden","no relation reaches this machine");let u;if(n!==void 0){try{u=m(n)}catch(a){return r(400,"invalid_request",a instanceof v?a.message:"invalid envelope")}if(u.act.sub!==t.sub||u.sub!==e.sub)return r(401,"unauthorized",l);if(u.act.act!==void 0&&u.act.act.place===null)return r(400,"invalid_request","the envelope names an app with no run place")}const s=u?.act.act,p=e.chain.slice(1).filter(a=>a.sub.startsWith("app:"));if(p.length>1)return r(401,"unauthorized","the token names more than one app");const d=p[0];if(d!==void 0&&(s===void 0||s.sub!==d.sub))return r(401,"unauthorized",l);const h=d??(s===void 0?void 0:{sub:s.sub});if(h!==void 0)try{await o.grantLink(h,e.sub)}catch(a){if(a instanceof f)return r(a.status,a.code,a.message);throw a}return u===void 0?{principal:{class:"machine",via:t.sub,sub:e.sub}}:s!==void 0?{principal:{class:"forwarded",via:t.sub,sub:e.sub,actor:{kind:"app",app:s.sub,node:s.node,node_scopes:s.node_scopes,place:s.place}}}:i!=="owner"?r(403,"forbidden","only the owner acts as the person"):{principal:{class:"forwarded",via:t.sub,sub:e.sub,actor:{kind:"person"}}}}c(L,"peerAuthority");export{G as mirrorGrantLink,L as peerAuthority};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { type
|
|
3
|
-
import { Router
|
|
2
|
+
import { type CallToken, type JwksCache } from '@north-light/identity';
|
|
3
|
+
import { Router } from '../api/router.js';
|
|
4
|
+
import type { GrantSync } from '../../core/grants/cover.js';
|
|
4
5
|
export interface PeerIngressContext {
|
|
5
6
|
/** The directory the machine tokens come from. */
|
|
6
7
|
issuer: string;
|
|
@@ -8,27 +9,28 @@ export interface PeerIngressContext {
|
|
|
8
9
|
jwks: JwksCache;
|
|
9
10
|
/** The audience machine tokens must carry: `machineUrl()` on the laptop, `runtime.audience` on the hosted runtime. Null before it is known: every request is refused 503. */
|
|
10
11
|
audience(): string | null;
|
|
11
|
-
/** The account owner (`user:<id>`) this runtime belongs to. Null when it has none: every request is refused
|
|
12
|
+
/** The account owner (`user:<id>`) this runtime belongs to. Null when it has none: no relation reaches the machine and every request is refused 403. */
|
|
12
13
|
owner(): string | null;
|
|
13
14
|
/** This machine's id (`runtime:<id>`). */
|
|
14
15
|
self(): string;
|
|
16
|
+
/** Pulls the directory's grants when a token's app link is ahead of the mirror. Absent: a mirror behind a token's `rev` answers 503. */
|
|
17
|
+
syncGrants?: GrantSync;
|
|
15
18
|
/** The router that serves the request; defaults to the daemon's own (`buildRouter(() => null)`), built on first use. */
|
|
16
19
|
router?: () => Router | Promise<Router>;
|
|
17
20
|
/** False while the JWKS or grants are not loaded: every request is refused 503 (audited). Defaults to ready. */
|
|
18
21
|
ready?: () => boolean;
|
|
19
22
|
}
|
|
20
|
-
/**
|
|
21
|
-
export declare function
|
|
22
|
-
principal: RequestPrincipal & {
|
|
23
|
-
class: 'forwarded' | 'machine';
|
|
24
|
-
};
|
|
25
|
-
} | {
|
|
26
|
-
status: number;
|
|
27
|
-
code: string;
|
|
28
|
-
message: string;
|
|
29
|
-
};
|
|
23
|
+
/** Whether the daemon's own route table serves this request: the routes `servePeer` hands to its router (`peer.*` and every daemon route a forwarded call reaches). */
|
|
24
|
+
export declare function isDaemonRoute(method: string, pathname: string): Promise<boolean>;
|
|
30
25
|
/** The account owner this runtime belongs to: the owner a successful `crtr sys join` recorded (a laptop), else — on a hosted runtime, which is
|
|
31
26
|
* provisioned without join — the one `sub` its mirrored grants all carry. Null when neither names exactly one owner. Never throws. */
|
|
32
27
|
export declare function runtimeOwner(): string | null;
|
|
33
|
-
/**
|
|
34
|
-
export
|
|
28
|
+
/** A bearer the caller already tried to verify and that failed: `servePeer` answers it as its own refusal (and audits it). */
|
|
29
|
+
export interface PeerRefusal {
|
|
30
|
+
status: number;
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
/** Serve one peer request; `verified` is a token the caller already checked against this runtime's audience, or the refusal its check ended in (so verification
|
|
35
|
+
* happens once, in the caller). Never throws. Every answer writes one `audit_rows` row: a request whose token never verified is recorded with `via` = `unauthenticated` and no identity. */
|
|
36
|
+
export declare function servePeer(req: IncomingMessage, res: ServerResponse, ctx: PeerIngressContext, verified?: CallToken | PeerRefusal): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var H=Object.defineProperty;var o=(e,t)=>H(e,"name",{value:t,configurable:!0});import{randomUUID as j}from"node:crypto";import{verifyCallToken as z}from"@north-light/identity";import{ENVELOPE_HEADER as I}from"../../core/peer/envelope.js";import{mirrorGrantLink as L,peerAuthority as U}from"./authority.js";import{peerVersionRefusal as q,PEER_VERSION_HEADER as D}from"../../core/peer/version.js";import{auditOutcome as v,writePeerAudit as G}from"../../core/audit.js";import{emitEvent as C}from"../../core/events/emit.js";import{joinedOwner as P}from"../../core/directory/join.js";import{allGrants as N}from"../../core/grants/mirror.js";import{errorCodes as T}from"../../api/error-codes.js";let O;function S(){return O??=import("../api/server.js").then(e=>e.buildRouter(()=>null)),O}o(S,"defaultRouter");async function x(e,t){return(await S()).match(e.toUpperCase(),t)!==null}o(x,"isDaemonRoute");function V(e,t,r,a,i,m){if(e.headersSent){e.destroy();return}const l=T[r];e.writeHead(t,{"content-type":"application/json; charset=utf-8","Request-Id":i,"X-Content-Type-Options":"nosniff"}),e.end(JSON.stringify({error:{code:r,message:a,...m===void 0?{}:{details:m},...l?{type:l.type}:{},request_id:i}}))}o(V,"refuse");const _=o((e,t)=>{const r=e.headers[t.toLowerCase()];return Array.isArray(r)?r[0]:r},"header");function ee(){try{const e=P();if(e!==null)return e;const t=new Set(N().map(r=>r.sub));return t.size===1?[...t][0]:null}catch{return null}}o(ee,"runtimeOwner");async function te(e,t,r,a){const i=t.getHeader("Request-Id")?.toString()??j();t.setHeader("Request-Id",i);const m=_(e,I),l=new URL(e.url??"/","http://peer.local"),E=(e.method??"GET").toUpperCase();let d={class:m===void 0?"machine":"forwarded",grantee:null,sub:null,via:"unauthenticated",operation:`${E} ${l.pathname}`,node_id:null},R=!1;const b=o((n,c={})=>{if(!R){R=!0;try{G({request_id:i,resource:l.pathname,...d,...c,outcome:n})}catch(u){C({level:"error",event:"daemon.peer.audit_failed",error:u instanceof Error?u:new Error(String(u)),fields:{request_id:i}})}}},"record"),s=o((n,c,u,f)=>{b(v(n)),V(t,n,c,u,i,f)},"deny");try{if(r.ready&&!r.ready()){s(503,"runtime_starting","runtime_starting");return}let n;if(a!==void 0&&!("chain"in a)){s(a.status,a.code,a.message);return}if(a!==void 0)n=a;else{const y=r.audience();if(y===null){s(503,"runtime_starting","this machine does not yet know its own address");return}const g=_(e,"authorization");if(!g?.startsWith("Bearer ")||g.length<=7){s(401,"unauthorized","a machine token is required");return}try{n=await z({token:g.slice(7),jwks:r.jwks,issuer:r.issuer,audience:y})}catch{s(401,"unauthorized","invalid machine token");return}}d={...d,grantee:n.sub,sub:n.sub,via:n.chain[0]?.sub??"unauthenticated"};const c=await(r.router??S)(),u=c.match(E,l.pathname);d={...d,operation:u?.operation.name??d.operation,node_id:u?.params.id??null};const f=r.owner(),p=await U(n,m,{self:r.self,relation:o(y=>f!==null&&y===f?"owner":null,"relation"),grantLink:L(r.syncGrants??(async()=>{}))});if("status"in p){s(p.status,p.code,p.message);return}const h=q(_(e,D));if(h!==null){s(426,h.code,h.message,h.details);return}const{principal:w}=p,k=w.class==="forwarded"&&w.actor.kind==="app"?w.actor:null,A={grantee:k?.app??n.sub,node_id:k?.node??d.node_id};t.once("close",()=>b(t.writableFinished?v(t.statusCode):"aborted",A)),await c.handle(e,t,null,w)}catch(n){C({level:"error",event:"daemon.peer.ingress_failed",error:n instanceof Error?n:new Error(String(n)),fields:{request_id:i}}),s(500,"server_failure","peer request failed")}}o(te,"servePeer");export{x as isDaemonRoute,ee as runtimeOwner,te as servePeer};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var U=Object.defineProperty;var o=(e,
|
|
1
|
+
var U=Object.defineProperty;var o=(e,i)=>U(e,"name",{value:i,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 G}from"../resume-detector.js";import{applySyncNotice as H,NoticeLedger as M}from"../api/sync-notice.js";import{runtimeOwner as J,servePeer as T}from"./ingress.js";import{startLocalIngress as D}from"./local-ingress.js";import{startRelayClient as L}from"./relay-client.js";const $="/v1/runtime/sync-notice",z=5e3;async function A(e,i,f){if(e.headersSent){e.destroy();return}e.writeHead(i,{"content-type":"application/json; charset=utf-8"}),e.end(JSON.stringify(f))}o(A,"answerJson");function oe(e){const i=e.unsandboxed??(()=>k("user").runtime?.unsandboxed===!0),f=e.issuer??b;let s;const S=new M;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 c=new E(r,fetch,e.jwksUrl?e.jwksUrl():k("user").runtime?.jwksUrl),d={issuer:r,jwks:c,audience:j,owner:J,self:R,ready:o(()=>O(r.replace(/\/$/,"")),"ready"),syncGrants:e.syncGrants,...e.ingress},w=await D(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 H(await S.accept(a.headers.authorization,c,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 T(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=G(),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:c},_({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"||!i()||r===null){await m();return}const c=t.reach.relay.replace(/\/$/,"");s?.relay!==c&&(await m(),await C(c,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};
|
|
@@ -7,6 +7,12 @@ description: Behavior changes in @north-light/sdk that an app must act on, newes
|
|
|
7
7
|
|
|
8
8
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
9
9
|
|
|
10
|
+
## 0.3.487
|
|
11
|
+
|
|
12
|
+
### Run notices and the app listener verify one call-token shape (additive, no app change)
|
|
13
|
+
|
|
14
|
+
`verifyRunNotice` and the runtime's app listener now verify bearers with `verifyCallToken` from `@north-light/identity`, which accepts the token shapes the directory mints today and the actor-chain shape it is moving to. A run notice still needs scope `crtr:run_notice`, your app as `grantee`, and a runtime as its actor, and is refused with 401 otherwise. Nothing an existing app sends or reads changes.
|
|
15
|
+
|
|
10
16
|
## 0.3.486
|
|
11
17
|
|
|
12
18
|
### `crtr:memory:read_private:user` and `oauth.permissionUrl` (additive, no app change)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.487",
|
|
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.
|
|
140
|
+
"@north-light/identity": "0.3.487",
|
|
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",
|
|
@@ -67,6 +67,7 @@ export declare class JwksCache {
|
|
|
67
67
|
private fetchKeys;
|
|
68
68
|
forKid(kid: string | undefined): Promise<JSONWebKeySet>;
|
|
69
69
|
}
|
|
70
|
+
/** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
|
|
70
71
|
export declare function verifyAccessToken(options: {
|
|
71
72
|
token: string;
|
|
72
73
|
jwks: JSONWebKeySet | JwksCache;
|
|
@@ -95,6 +96,7 @@ export interface MachineTokenClaims extends JWTPayload {
|
|
|
95
96
|
* refused, so an app grant token never verifies here; `verifyAccessToken` requires an `app:` grantee, so a machine
|
|
96
97
|
* token never verifies there.
|
|
97
98
|
*/
|
|
99
|
+
/** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
|
|
98
100
|
export declare function verifyMachineToken(options: {
|
|
99
101
|
token: string;
|
|
100
102
|
jwks: JSONWebKeySet | JwksCache;
|
|
@@ -133,12 +135,65 @@ export interface OutboundTokenClaims extends JWTPayload {
|
|
|
133
135
|
* an `app:` or `user:` principal; the remaining claims are checked for presence and type only; which of them gates a
|
|
134
136
|
* call is the caller's rule (D3-TOK-13 gates on `scope` alone, and nothing gates on `act`).
|
|
135
137
|
*/
|
|
138
|
+
/** @deprecated Use `verifyCallToken`; removed after the peer-token rollout window (plan I2). */
|
|
136
139
|
export declare function verifyOutboundToken(options: {
|
|
137
140
|
token: string;
|
|
138
141
|
jwks: JSONWebKeySet | JwksCache;
|
|
139
142
|
issuer: string;
|
|
140
143
|
audience: string;
|
|
141
144
|
}): Promise<OutboundTokenClaims>;
|
|
145
|
+
/** One link of a call token's actor chain. `grant` and `rev` appear only on an `app:` link. */
|
|
146
|
+
export interface CallLink {
|
|
147
|
+
sub: string;
|
|
148
|
+
grant?: string;
|
|
149
|
+
rev?: number;
|
|
150
|
+
}
|
|
151
|
+
/** The verified payload of a call token. Fields beyond `sub`/`aud`/`client_id` are present on some uses only. */
|
|
152
|
+
export interface CallTokenClaims extends JWTPayload {
|
|
153
|
+
iss: string;
|
|
154
|
+
aud: string;
|
|
155
|
+
/** The caller's person, `user:<id>`. */
|
|
156
|
+
sub: string;
|
|
157
|
+
client_id: string;
|
|
158
|
+
exp: number;
|
|
159
|
+
iat: number;
|
|
160
|
+
/** The RFC 8693 actor chain as signed; absent on the legacy shapes. */
|
|
161
|
+
act?: unknown;
|
|
162
|
+
scope?: string;
|
|
163
|
+
/** Legacy same-account machine token (removed at the flag flip). */
|
|
164
|
+
machine_id?: string;
|
|
165
|
+
grantee?: string;
|
|
166
|
+
initiating_app?: string;
|
|
167
|
+
grant?: string;
|
|
168
|
+
rev?: number;
|
|
169
|
+
}
|
|
170
|
+
/** A verified call token. The verifier authorizes nothing: the caller decides authority from `chain`. */
|
|
171
|
+
export interface CallToken {
|
|
172
|
+
/** `user:<id>`. */
|
|
173
|
+
sub: string;
|
|
174
|
+
aud: string;
|
|
175
|
+
/** The actor chain, index 0 = the presenter. Legacy shapes are normalized to the same form. */
|
|
176
|
+
chain: CallLink[];
|
|
177
|
+
/** Present only on provider tokens. */
|
|
178
|
+
scope?: string;
|
|
179
|
+
claims: CallTokenClaims;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Verifies a call token of any use (peer, app access, outbound provider, run notice) and returns its actor chain,
|
|
183
|
+
* presenter first. Checks in order: header `typ` `at+jwt`, `alg` ES256 or RS256, non-empty `kid`; signature; `iss`;
|
|
184
|
+
* `aud` a string equal to `audience`; `exp` and `iat` numbers; `sub` = `user:<id>`; `client_id` non-empty; `scope`
|
|
185
|
+
* a string when present; then the chain. Without `act` the legacy machine token (`machine_id`, `grantee` = `sub`)
|
|
186
|
+
* and the legacy app token (`app:` `grantee` + `grant` + `rev`) are normalized to a one-link chain; with `act` any
|
|
187
|
+
* `machine_id` must equal the first link, any top-level `grant` an app link's, and, on a token without `scope`, any `grantee`
|
|
188
|
+
* and `rev` the app link's (or, with no app link, `grantee` = `sub` and no `rev`). It authorizes nothing.
|
|
189
|
+
* Throws `MissingRevClaimError` for an `app:` link carrying `grant` and no `rev`.
|
|
190
|
+
*/
|
|
191
|
+
export declare function verifyCallToken(options: {
|
|
192
|
+
token: string;
|
|
193
|
+
jwks: JSONWebKeySet | JwksCache;
|
|
194
|
+
issuer: string;
|
|
195
|
+
audience: string;
|
|
196
|
+
}): Promise<CallToken>;
|
|
142
197
|
export interface SyncNoticeClaims extends JWTPayload {
|
|
143
198
|
purpose: 'grant_sync';
|
|
144
199
|
iat: number;
|