@portalsdk/cli 0.3.1 → 0.4.1

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 (2) hide show
  1. package/dist/index.js +89 -7
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,7 +1,89 @@
1
1
  #!/usr/bin/env node
2
- import{createRequire as at}from"module";import{Command as ct}from"commander";import c from"picocolors";import{spawn as we}from"child_process";import ve from"os";var d=class extends Error{constructor(t,r,o){super(t);this.status=r;this.code=o;this.name="ApiError"}},x=class{constructor(n,t){this.secret=t;this.baseUrl=n.replace(/\/+$/,"")}baseUrl;async createDeploy(n){let{status:t,data:r}=await this.send("POST","/v1/deploys",n);if(t>=200&&t<300)return{versionId:H(r),alreadyDeployed:!1};if(t===409)return{versionId:H(r),alreadyDeployed:!0};throw this.fail(t,r)}async activate(n){let{status:t,data:r}=await this.send("POST",`/v1/deploys/${encodeURIComponent(n)}/activate`);if(t<200||t>=300)throw this.fail(t,r)}async setSecret(n,t){let{status:r,data:o}=await this.send("PUT",`/v1/secrets/${encodeURIComponent(n)}`,{value:t});if(r<200||r>=300)throw this.fail(r,o)}async send(n,t,r){let o;try{o=await fetch(`${this.baseUrl}${t}`,{method:n,headers:{authorization:`Bearer ${this.secret}`,"content-type":"application/json"},body:r===void 0?void 0:JSON.stringify(r)})}catch(a){throw new d(`Could not reach the Portal API at ${this.baseUrl}: ${a instanceof Error?a.message:String(a)}`,0)}let s=await o.text(),i={};if(s)try{i=JSON.parse(s)}catch{i={reason:s}}return{status:o.status,data:i}}fail(n,t){let r=t.code;return n===401||r==="unauthorized"?new d("Your PORTAL_SECRET was not accepted. It must be a valid project secret key (sk_...).",n,r):new d(t.reason||`The Portal API rejected the request (${r??n}).`,n,r)}};function H(e){if(!e.versionId)throw new d("The Portal API did not return a version id for this deploy.",502);return e.versionId}var u=class{baseUrl;constructor(n){this.baseUrl=n.replace(/\/+$/,"")}async startDeviceLogin(n){let{status:t,data:r}=await this.send("POST","/v1/device/start",{...n?{name:n}:{}});if(t<200||t>=300)throw this.fail(t,r);let o=r;if(!o.deviceCode||!o.userCode||!o.verificationUri)throw new d("The Portal API did not return a usable login code.",502);return{deviceCode:o.deviceCode,userCode:o.userCode,verificationUri:o.verificationUri,interval:typeof o.interval=="number"?o.interval:5,expiresIn:typeof o.expiresIn=="number"?o.expiresIn:600}}async pollDeviceLogin(n){let{status:t,data:r}=await this.send("POST","/v1/device/poll",{deviceCode:n});if(t===429)return{status:"slow_down"};if(t===404)return{status:"expired"};if(t<200||t>=300)throw this.fail(t,r);let o=r;switch(o.status){case"approved":return{status:"approved",token:o.token,expiresAt:o.expiresAt};case"denied":return{status:"denied"};case"expired":return{status:"expired"};default:return{status:"pending"}}}async fetchIdentity(n){let{status:t,data:r}=await this.send("GET","/v1/me",void 0,n);if(t<200||t>=300)throw this.fail(t,r);return r}async revokeCurrentToken(n){let{status:t,data:r}=await this.send("DELETE","/v1/cli-tokens/current",void 0,n);if(t<200||t>=300)throw this.fail(t,r)}async revokeAllTokens(n){let{status:t,data:r}=await this.send("DELETE","/v1/cli-tokens",void 0,n);if(t<200||t>=300)throw this.fail(t,r);let o=r;return typeof o.revoked=="number"?o.revoked:0}async createProject(n,t,r){let{status:o,data:s}=await this.send("POST","/v1/projects",{organizationId:t,name:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.project?.id||!Array.isArray(i.environments))throw new d("The Portal API did not return a usable project.",502);return{project:i.project,environments:i.environments}}async createApiKey(n,t,r){let{status:o,data:s}=await this.send("POST",`/v1/environments/${encodeURIComponent(t)}/api-keys`,{keyType:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.apiKey?.value)throw new d("The Portal API did not return a usable API key.",502);return i.apiKey}async createAllowedOrigin(n,t,r){let{status:o,data:s}=await this.send("POST",`/v1/environments/${encodeURIComponent(t)}/allowed-origins`,{origin:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.origin?.id)throw new d("The Portal API did not return a usable origin.",502);return i.origin}async listAllowedOrigins(n,t){let{status:r,data:o}=await this.send("GET",`/v1/environments/${encodeURIComponent(t)}/allowed-origins`,void 0,n);if(r<200||r>=300)throw this.fail(r,o);let s=o;return Array.isArray(s.origins)?s.origins:[]}async deleteAllowedOrigin(n,t){let{status:r,data:o}=await this.send("DELETE",`/v1/allowed-origins/${encodeURIComponent(t)}`,void 0,n);if(r<200||r>=300)throw this.fail(r,o)}async send(n,t,r,o){let s;try{s=await fetch(`${this.baseUrl}${t}`,{method:n,headers:{"content-type":"application/json",...o?{authorization:`Bearer ${o}`}:{}},body:r===void 0?void 0:JSON.stringify(r)})}catch(l){throw new d(`Could not reach the Portal API at ${this.baseUrl}: ${l instanceof Error?l.message:String(l)}`,0)}let i=await s.text(),a={};if(i)try{a=JSON.parse(i)}catch{a={reason:i}}return{status:s.status,data:a}}fail(n,t){let r=typeof t.code=="string"?t.code:void 0,o=typeof t.reason=="string"?t.reason:void 0;return n===401?new d("Your login is no longer valid.",n,r):new d(o||`The Portal API rejected the request (${r??n}).`,n,r)}};import k from"fs/promises";import P from"os";import F from"path";var ye=448,z=384;function W(e=P.homedir()){return F.join(e,".portal")}function $(e=P.homedir()){return F.join(W(e),"credentials.json")}async function S(e=P.homedir()){let n;try{n=await k.readFile($(e),"utf8")}catch{return null}try{let t=JSON.parse(n);return typeof t.token!="string"||!t.token?null:{token:t.token,expiresAt:typeof t.expiresAt=="string"?t.expiresAt:void 0}}catch{return null}}async function B(e,n=P.homedir()){let t=W(n),r=$(n);return await k.mkdir(t,{recursive:!0,mode:ye}),await k.writeFile(r,`${JSON.stringify(e,null,2)}
3
- `,{mode:z}),await k.chmod(r,z),r}async function j(e=P.homedir()){try{await k.unlink($(e))}catch{}}var xe=e=>new Promise(n=>setTimeout(n,e));function ke(e){return e.length===8?`${e.slice(0,4)}-${e.slice(4)}`:e}function Pe(){try{return ve.hostname()||void 0}catch{return}}function be(e){let n=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",t=process.platform==="win32"?["/c","start","",e]:[e];try{let r=we(n,t,{stdio:"ignore",detached:!0});r.on("error",()=>{}),r.unref()}catch{}}async function J(e){let n=e.sleep??xe,t=e.openBrowser??be,r=e.log??(p=>console.log(p)),o=new u(e.apiUrl),s=e.name??Pe(),i=await o.startDeviceLogin(s),a=`${i.verificationUri}?code=${encodeURIComponent(i.userCode)}`;r(""),r(` Your code: ${ke(i.userCode)}`),r(` Approve at: ${a}`),r(""),r(" Waiting for approval\u2026"),t(a);let l=Date.now()+i.expiresIn*1e3,f=i.interval*1e3;for(;Date.now()<l;){await n(f);let p=await o.pollDeviceLogin(i.deviceCode);if(p.status==="slow_down"){f+=5e3;continue}if(p.status==="pending")continue;if(p.status==="denied")throw new Error("The login request was denied.");if(p.status==="expired")throw new Error("The login request expired. Run `portal login` to try again.");if(!p.token)throw new Error("This login code was already used. Run `portal login` to start a new one.");let w=await B({token:p.token,expiresAt:p.expiresAt}),O=null;try{O=await o.fetchIdentity(p.token)}catch{O=null}return{identity:O,credentialsPath:w}}throw new Error("The login request expired. Run `portal login` to try again.")}async function G(e){let n=await S();if(!n)return{loggedIn:!1};try{return{loggedIn:!0,identity:await new u(e.apiUrl).fetchIdentity(n.token)}}catch(t){if(t instanceof d&&t.status===401)return await j(),{loggedIn:!1};throw t}}async function V(e){let n=await S();if(!n)return{wasLoggedIn:!1,revoked:!1};let t=!1,r;try{let o=new u(e.apiUrl);e.all?r=await o.revokeAllTokens(n.token):await o.revokeCurrentToken(n.token),t=!0}catch{}return await j(),{wasLoggedIn:!0,revoked:t,...r!==void 0?{revokedCount:r}:{}}}async function Ee(){return(await S())?.token??null}async function v(){let e=await Ee();if(!e)throw new Error("Not signed in. Run: portal login");return e}import Je from"path";import{existsSync as X}from"fs";import T from"path";import{mkdtemp as Se,rm as Ce,writeFile as Ae}from"fs/promises";import{tmpdir as Ie}from"os";import C from"path";import{pathToFileURL as Oe}from"url";import Y from"esbuild";var U=class extends Error{};async function A(e){let n;try{n=(await Y.build({entryPoints:[e],bundle:!0,write:!1,format:"esm",platform:"node",target:"node18",logLevel:"silent",absWorkingDir:C.dirname(e)})).outputFiles[0].text}catch(o){throw new U($e(o,e))}let t=await Se(C.join(Ie(),"portal-cli-")),r=C.join(t,"module.mjs");try{return await Ae(r,n,"utf8"),await import(Oe(r).href)}finally{await Ce(t,{recursive:!0,force:!0})}}function $e(e,n){if(e!=null&&typeof e=="object"&&"errors"in e&&Array.isArray(e.errors)){let r=Y.formatMessagesSync(e.errors,{kind:"error",color:!1}).join(`
4
- `).trimEnd();if(r)return r}return`Failed to compile ${C.basename(n)}: ${e instanceof Error?e.message:String(e)}`}var b=class extends Error{},je=["portal.config.ts","portal.config.mts","portal.config.js","portal.config.mjs"];function Q(e,n){if(n){let t=T.resolve(e,n);if(!X(t))throw new b(`No config file found at ${n}`);return t}for(let t of je){let r=T.join(e,t);if(X(r))return r}throw new b(`No portal.config.ts found in ${e}. Create one, or point at it with --config <path>.`)}async function Z(e){let t=(await A(e)).default;if(t==null||typeof t!="object")throw new b(`${T.basename(e)} must \`export default defineConfig({ ... })\`.`);return t}function L(e,n){let t={project:{webhooks:null},channels:{}};e.auth&&(t.project.auth={issuer:e.auth.issuer,jwksUrl:e.auth.jwksUrl,claimMap:{...e.auth.claimMap}});for(let[r,o]of Object.entries(e.channels??{}))t.channels[r]=Ue(o,n.get(r)??[]);return t}function Ue(e,n){let t={};e.authz&&(t.authz=!0),e.onPublish&&e.onPublish.length>0&&(t.onPublish=e.onPublish.length),e.onDisconnect&&e.onDisconnect.length>0&&(t.onDisconnect=e.onDisconnect.length),e.notify&&(t.notify=!0);let r={};for(let o of n)r[o.handle]={namespace:o.namespace,transport:o.transport};return{mode:e.mode??"standard",anonymous:e.anonymous??!0,hooks:t,extensions:r}}import{mkdtemp as Re,rm as De,writeFile as Me}from"fs/promises";import{tmpdir as Ne}from"os";import I from"path";import qe from"esbuild";var y=class extends Error{};function R(e,n){if(e.webhooks!=null)throw new y("The `webhooks` block is reserved for a future release. Remove it before deploying.");for(let[t,r]of Object.entries(e.channels??{})){if(r&&"onMiddlewareFailure"in r)throw new y(`Channel "${t}" sets \`onMiddlewareFailure\`, which is no longer supported \u2014 message handling always fails closed. Remove it.`);Te(t),Le(t,n.get(t)??[])}}function Te(e){if(e.includes("*")&&!/^[^*]+\*$/.test(e))throw new y(`Unsupported channel pattern "${e}": a template must end with a single "*" over a fixed prefix (e.g. "room-*").`)}function Le(e,n){let t=new Map;for(let r of n){let o=t.get(r.namespace);if(o)throw new y(`Channel "${e}" has two extensions using the namespace "${r.namespace}" ("${o}" and "${r.handle}"). Each namespace must be unique within a channel.`);t.set(r.namespace,r.handle)}}var Ke="hooks";function te(e){return`import config from ${JSON.stringify(e)};
2
+ import{createRequire as ft}from"module";import{Command as gt}from"commander";import c from"picocolors";import{spawn as ke}from"child_process";import be from"os";var d=class extends Error{constructor(t,r,o){super(t);this.status=r;this.code=o;this.name="ApiError"}},v=class{constructor(n,t){this.secret=t;this.baseUrl=n.replace(/\/+$/,"")}baseUrl;async createDeploy(n){let{status:t,data:r}=await this.send("POST","/v1/deploys",n);if(t>=200&&t<300)return{versionId:H(r),alreadyDeployed:!1};if(t===409)return{versionId:H(r),alreadyDeployed:!0};throw this.fail(t,r)}async activate(n){let{status:t,data:r}=await this.send("POST",`/v1/deploys/${encodeURIComponent(n)}/activate`);if(t<200||t>=300)throw this.fail(t,r)}async setSecret(n,t){let{status:r,data:o}=await this.send("PUT",`/v1/secrets/${encodeURIComponent(n)}`,{value:t});if(r<200||r>=300)throw this.fail(r,o)}async send(n,t,r){let o;try{o=await fetch(`${this.baseUrl}${t}`,{method:n,headers:{authorization:`Bearer ${this.secret}`,"content-type":"application/json"},body:r===void 0?void 0:JSON.stringify(r)})}catch(a){throw new d(`Could not reach the Portal API at ${this.baseUrl}: ${a instanceof Error?a.message:String(a)}`,0)}let s=await o.text(),i={};if(s)try{i=JSON.parse(s)}catch{i={reason:s}}return{status:o.status,data:i}}fail(n,t){let r=t.code;return n===401||r==="unauthorized"?new d("Your PORTAL_SECRET was not accepted. It must be a valid project secret key (sk_...).",n,r):new d(t.reason||`The Portal API rejected the request (${r??n}).`,n,r)}};function H(e){if(!e.versionId)throw new d("The Portal API did not return a version id for this deploy.",502);return e.versionId}var u=class{baseUrl;constructor(n){this.baseUrl=n.replace(/\/+$/,"")}async startDeviceLogin(n){let{status:t,data:r}=await this.send("POST","/v1/device/start",{...n?{name:n}:{}});if(t<200||t>=300)throw this.fail(t,r);let o=r;if(!o.deviceCode||!o.userCode||!o.verificationUri)throw new d("The Portal API did not return a usable login code.",502);return{deviceCode:o.deviceCode,userCode:o.userCode,verificationUri:o.verificationUri,interval:typeof o.interval=="number"?o.interval:5,expiresIn:typeof o.expiresIn=="number"?o.expiresIn:600}}async pollDeviceLogin(n){let{status:t,data:r}=await this.send("POST","/v1/device/poll",{deviceCode:n});if(t===429)return{status:"slow_down"};if(t===404)return{status:"expired"};if(t<200||t>=300)throw this.fail(t,r);let o=r;switch(o.status){case"approved":return{status:"approved",token:o.token,expiresAt:o.expiresAt};case"denied":return{status:"denied"};case"expired":return{status:"expired"};default:return{status:"pending"}}}async fetchIdentity(n){let{status:t,data:r}=await this.send("GET","/v1/me",void 0,n);if(t<200||t>=300)throw this.fail(t,r);return r}async revokeCurrentToken(n){let{status:t,data:r}=await this.send("DELETE","/v1/cli-tokens/current",void 0,n);if(t<200||t>=300)throw this.fail(t,r)}async revokeAllTokens(n){let{status:t,data:r}=await this.send("DELETE","/v1/cli-tokens",void 0,n);if(t<200||t>=300)throw this.fail(t,r);let o=r;return typeof o.revoked=="number"?o.revoked:0}async createProject(n,t,r){let{status:o,data:s}=await this.send("POST","/v1/projects",{organizationId:t,name:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.project?.id||!Array.isArray(i.environments))throw new d("The Portal API did not return a usable project.",502);return{project:i.project,environments:i.environments}}async createApiKey(n,t,r){let{status:o,data:s}=await this.send("POST",`/v1/environments/${encodeURIComponent(t)}/api-keys`,{keyType:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.apiKey?.value)throw new d("The Portal API did not return a usable API key.",502);return i.apiKey}async createAllowedOrigin(n,t,r){let{status:o,data:s}=await this.send("POST",`/v1/environments/${encodeURIComponent(t)}/allowed-origins`,{origin:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.origin?.id)throw new d("The Portal API did not return a usable origin.",502);return i.origin}async listAllowedOrigins(n,t){let{status:r,data:o}=await this.send("GET",`/v1/environments/${encodeURIComponent(t)}/allowed-origins`,void 0,n);if(r<200||r>=300)throw this.fail(r,o);let s=o;return Array.isArray(s.origins)?s.origins:[]}async deleteAllowedOrigin(n,t){let{status:r,data:o}=await this.send("DELETE",`/v1/allowed-origins/${encodeURIComponent(t)}`,void 0,n);if(r<200||r>=300)throw this.fail(r,o)}async send(n,t,r,o){let s;try{s=await fetch(`${this.baseUrl}${t}`,{method:n,headers:{"content-type":"application/json",...o?{authorization:`Bearer ${o}`}:{}},body:r===void 0?void 0:JSON.stringify(r)})}catch(l){throw new d(`Could not reach the Portal API at ${this.baseUrl}: ${l instanceof Error?l.message:String(l)}`,0)}let i=await s.text(),a={};if(i)try{a=JSON.parse(i)}catch{a={reason:i}}return{status:s.status,data:a}}fail(n,t){let r=typeof t.code=="string"?t.code:void 0,o=typeof t.reason=="string"?t.reason:void 0;return n===401?new d("Your login is no longer valid.",n,r):new d(o||`The Portal API rejected the request (${r??n}).`,n,r)}};import k from"fs/promises";import b from"os";import F from"path";var ve=448,B=384;function J(e=b.homedir()){return F.join(e,".portal")}function $(e=b.homedir()){return F.join(J(e),"credentials.json")}async function S(e=b.homedir()){let n;try{n=await k.readFile($(e),"utf8")}catch{return null}try{let t=JSON.parse(n);return typeof t.token!="string"||!t.token?null:{token:t.token,expiresAt:typeof t.expiresAt=="string"?t.expiresAt:void 0}}catch{return null}}async function G(e,n=b.homedir()){let t=J(n),r=$(n);return await k.mkdir(t,{recursive:!0,mode:ve}),await k.writeFile(r,`${JSON.stringify(e,null,2)}
3
+ `,{mode:B}),await k.chmod(r,B),r}async function j(e=b.homedir()){try{await k.unlink($(e))}catch{}}var Ee=e=>new Promise(n=>setTimeout(n,e));function Pe(e){return e.length===8?`${e.slice(0,4)}-${e.slice(4)}`:e}function Se(){try{return be.hostname()||void 0}catch{return}}function Oe(e){let n=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",t=process.platform==="win32"?["/c","start","",e]:[e];try{let r=ke(n,t,{stdio:"ignore",detached:!0});r.on("error",()=>{}),r.unref()}catch{}}async function V(e){let n=e.sleep??Ee,t=e.openBrowser??Oe,r=e.log??(p=>console.log(p)),o=new u(e.apiUrl),s=e.name??Se(),i=await o.startDeviceLogin(s),a=`${i.verificationUri}?code=${encodeURIComponent(i.userCode)}`;r(""),r(` Your code: ${Pe(i.userCode)}`),r(` Approve at: ${a}`),r(""),r(" Waiting for approval\u2026"),t(a);let l=Date.now()+i.expiresIn*1e3,f=i.interval*1e3;for(;Date.now()<l;){await n(f);let p=await o.pollDeviceLogin(i.deviceCode);if(p.status==="slow_down"){f+=5e3;continue}if(p.status==="pending")continue;if(p.status==="denied")throw new Error("The login request was denied.");if(p.status==="expired")throw new Error("The login request expired. Run `portal login` to try again.");if(!p.token)throw new Error("This login code was already used. Run `portal login` to start a new one.");let w=await G({token:p.token,expiresAt:p.expiresAt}),A=null;try{A=await o.fetchIdentity(p.token)}catch{A=null}return{identity:A,credentialsPath:w}}throw new Error("The login request expired. Run `portal login` to try again.")}async function Y(e){let n=await S();if(!n)return{loggedIn:!1};try{return{loggedIn:!0,identity:await new u(e.apiUrl).fetchIdentity(n.token)}}catch(t){if(t instanceof d&&t.status===401)return await j(),{loggedIn:!1};throw t}}async function X(e){let n=await S();if(!n)return{wasLoggedIn:!1,revoked:!1};let t=!1,r;try{let o=new u(e.apiUrl);e.all?r=await o.revokeAllTokens(n.token):await o.revokeCurrentToken(n.token),t=!0}catch{}return await j(),{wasLoggedIn:!0,revoked:t,...r!==void 0?{revokedCount:r}:{}}}async function Ie(){return(await S())?.token??null}async function x(){let e=await Ie();if(!e)throw new Error("Not signed in. Run: portal login");return e}import Ze from"path";import{existsSync as Z}from"fs";import T from"path";import{mkdtemp as Ce,rm as Ae,writeFile as $e}from"fs/promises";import{tmpdir as je}from"os";import O from"path";import{pathToFileURL as Ue}from"url";import Q from"esbuild";var U=class extends Error{};async function I(e){let n;try{n=(await Q.build({entryPoints:[e],bundle:!0,write:!1,format:"esm",platform:"node",target:"node18",logLevel:"silent",absWorkingDir:O.dirname(e)})).outputFiles[0].text}catch(o){throw new U(Te(o,e))}let t=await Ce(O.join(je(),"portal-cli-")),r=O.join(t,"module.mjs");try{return await $e(r,n,"utf8"),await import(Ue(r).href)}finally{await Ae(t,{recursive:!0,force:!0})}}function Te(e,n){if(e!=null&&typeof e=="object"&&"errors"in e&&Array.isArray(e.errors)){let r=Q.formatMessagesSync(e.errors,{kind:"error",color:!1}).join(`
4
+ `).trimEnd();if(r)return r}return`Failed to compile ${O.basename(n)}: ${e instanceof Error?e.message:String(e)}`}var E=class extends Error{},Re=["portal.config.ts","portal.config.mts","portal.config.js","portal.config.mjs"];function ee(e,n){if(n){let t=T.resolve(e,n);if(!Z(t))throw new E(`No config file found at ${n}`);return t}for(let t of Re){let r=T.join(e,t);if(Z(r))return r}throw new E(`No portal.config.ts found in ${e}. Create one, or point at it with --config <path>.`)}async function te(e){let t=(await I(e)).default;if(t==null||typeof t!="object")throw new E(`${T.basename(e)} must \`export default defineConfig({ ... })\`.`);return t}function R(e,n){let t={project:{webhooks:null},channels:{}};e.auth&&(t.project.auth={issuer:e.auth.issuer,jwksUrl:e.auth.jwksUrl,claimMap:{...e.auth.claimMap}});for(let[r,o]of Object.entries(e.channels??{}))t.channels[r]=Le(o,n.get(r)??[]);return t}function Le(e,n){let t={};e.authz&&(t.authz=!0),e.onPublish&&e.onPublish.length>0&&(t.onPublish=e.onPublish.length),e.onDisconnect&&e.onDisconnect.length>0&&(t.onDisconnect=e.onDisconnect.length),e.notify&&(t.notify=!0);let r={};for(let o of n)r[o.handle]={namespace:o.namespace,transport:o.transport};return{mode:e.mode??"standard",anonymous:e.anonymous??!0,hooks:t,extensions:r}}import{mkdtemp as Me,rm as _e,writeFile as ze}from"fs/promises";import{tmpdir as Ke}from"os";import C from"path";import We from"esbuild";var y=class extends Error{},L=5;function D(e,n){if(e.webhooks!=null)throw new y("The `webhooks` block is reserved for a future release. Remove it before deploying.");for(let[t,r]of Object.entries(e.channels??{})){if(r&&"onMiddlewareFailure"in r)throw new y(`Channel "${t}" sets \`onMiddlewareFailure\`, which is no longer supported \u2014 message handling always fails closed. Remove it.`);De(t),Ne(t,n.get(t)??[]),qe(t,n.get(t)??[])}}function De(e){if(e.includes("*")&&!/^[^*]+\*$/.test(e))throw new y(`Unsupported channel pattern "${e}": a template must end with a single "*" over a fixed prefix (e.g. "room-*").`)}function Ne(e,n){if(n.length<=L)return;let t=n.map(r=>`"${r.handle}"`).join(", ");throw new y(`Channel "${e}" attaches ${n.length} extensions (${t}), but a channel may carry at most ${L}. Remove ${n.length-L} of them, or split the channel.`)}function qe(e,n){let t=new Map;for(let r of n){let o=t.get(r.namespace);if(o)throw new y(`Channel "${e}" has two extensions using the namespace "${r.namespace}" ("${o}" and "${r.handle}"). Each namespace must be unique within a channel.`);t.set(r.namespace,r.handle)}}var He="hooks",N="PortalExtension";function re(e){return`import Extension from ${JSON.stringify(e)};
5
+
6
+ export class ${N} {
7
+ constructor(ctx, env) {
8
+ this.ctx = ctx;
9
+ this.instance = new Extension(ctx, env);
10
+ this.initialized = null;
11
+ }
12
+
13
+ // onInit must complete before the first onBatch. The platform dispatches init and the
14
+ // first batch independently, so ordering is enforced here rather than assumed: whichever
15
+ // arrives first triggers init, and everything else awaits that same promise.
16
+ //
17
+ // On success the promise is cached, so onInit runs exactly once. On failure the cache is
18
+ // cleared so the next dispatch retries \u2014 an instance lives as long as its channel, and a
19
+ // transient failure (a cold database, a blipped fetch) must not strand it until eviction.
20
+ // The rejection is still rethrown, so the dispatch that hit it fails closed rather than
21
+ // running onBatch against half-initialized state.
22
+ ready(request) {
23
+ if (!this.initialized) {
24
+ const init = { kind: "init", channelId: request.channelId, epoch: request.epoch };
25
+ this.initialized = Promise.resolve(
26
+ typeof this.instance.onInit === "function" ? this.instance.onInit(init) : undefined,
27
+ ).catch((error) => {
28
+ this.initialized = null;
29
+ throw error;
30
+ });
31
+ }
32
+ return this.initialized;
33
+ }
34
+
35
+ async fetch(request) {
36
+ const message = await request.json();
37
+ await this.ready(message);
38
+
39
+ switch (message.kind) {
40
+ case "init":
41
+ return Response.json({});
42
+
43
+ case "window": {
44
+ const batch = {
45
+ kind: "batch",
46
+ channelId: message.channelId,
47
+ epoch: message.epoch,
48
+ batchSeq: message.windowSeq,
49
+ messages: message.messages || [],
50
+ };
51
+ const result = await this.instance.onBatch(batch);
52
+ return Response.json(result || {});
53
+ }
54
+
55
+ case "snapshot": {
56
+ if (typeof this.instance.onSnapshot !== "function") return Response.json({ snapshot: null });
57
+ const result = await this.instance.onSnapshot(message);
58
+ return Response.json(result || { snapshot: null });
59
+ }
60
+
61
+ case "shutdown": {
62
+ if (typeof this.instance.onShutdown === "function") await this.instance.onShutdown(message);
63
+ return Response.json({});
64
+ }
65
+
66
+ default:
67
+ return Response.json({ error: "unknown request kind: " + message.kind }, { status: 400 });
68
+ }
69
+ }
70
+ }
71
+
72
+ export default {
73
+ async fetch(request, env) {
74
+ const message = await request.json();
75
+ // One durable instance per channel.
76
+ const id = env.EXTENSION.idFromName(message.channelId);
77
+ return env.EXTENSION.get(id).fetch(
78
+ new Request("https://portal-extension.internal/run", {
79
+ method: "POST",
80
+ headers: { "content-type": "application/json" },
81
+ body: JSON.stringify(message),
82
+ }),
83
+ );
84
+ },
85
+ };
86
+ `}function oe(e){return`import config from ${JSON.stringify(e)};
5
87
 
6
88
  const channels = config.channels || {};
7
89
 
@@ -50,10 +132,10 @@ export async function run(request) {
50
132
  }
51
133
 
52
134
  export default { run };
53
- `}function ne(e){return Object.values(e.channels??{}).some(n=>n.authz!=null||(n.onPublish?.length??0)>0||(n.onDisconnect?.length??0)>0||n.notify!=null)}async function D(e,n,t){let r=I.dirname(n),o;if(ne(e)){let i=await He(n);o={name:Ke,content:i}}let s=[];for(let[i,a]of t)for(let l of a){let f=await re(l.path,r);s.push({key:i,handle:l.handle,name:ze(i,l.handle),content:f})}return _e(s),{hookScript:o,extensionScripts:s}}function _e(e){let n=new Map;for(let t of e){let r=n.get(t.name);if(r)throw new y(`Extensions "${r.handle}" (channel "${r.key}") and "${t.handle}" (channel "${t.key}") both map to the upload name "${t.name}". Rename a handle or channel so they differ by more than punctuation.`);n.set(t.name,t)}}function M(e,n){for(let t of n){let r=e.channels[t.key]?.extensions[t.handle];r&&(r.script=t.name)}}async function He(e){let n=await Re(I.join(Ne(),"portal-cli-hooks-")),t=I.join(n,"hooks.mjs");try{return await Me(t,te(e),"utf8"),await re(t,I.dirname(e))}finally{await De(n,{recursive:!0,force:!0})}}async function re(e,n){return(await qe.build({entryPoints:[e],bundle:!0,write:!1,format:"esm",target:"es2022",platform:"neutral",logLevel:"silent",absWorkingDir:n})).outputFiles[0].text}function ze(e,n){return`ext-${ee(e)}-${ee(n)}`}function ee(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}import{existsSync as Fe}from"fs";import We from"path";var E=class extends Error{};async function N(e,n){let t=new Map;for(let[r,o]of Object.entries(e.channels??{})){let s=o.extensions;if(!s||Object.keys(s).length===0)continue;let i=[];for(let[a,l]of Object.entries(s)){let f=We.resolve(n,l);if(!Fe(f))throw new E(`Channel "${r}" attaches extension "${a}" from "${l}", but that file does not exist.`);let p=await A(f),w=Be(p.default);if(!w)throw new E(`Extension "${a}" (${l}) must \`export default defineExtension(...)\` with a static manifest.`);i.push({handle:a,source:l,namespace:w.namespace,transport:w.transport,path:f})}t.set(r,i)}return t}function Be(e){if(typeof e!="function"&&typeof e!="object")return null;let n=e?.manifest;if(n==null||typeof n!="object")return null;let{namespace:t,transport:r}=n;return typeof t!="string"||!t.endsWith(".")||r!=="ws"&&r!=="http"?null:{namespace:t,transport:r}}async function oe(e){let n=Q(e.cwd,e.configPath),t=await Z(n),r=Je.dirname(n),o=await N(t,r);R(t,o);let s=L(t,o),i=await D(t,n,o);M(s,i.extensionScripts);let a=new x(e.apiUrl,e.secret),{versionId:l,alreadyDeployed:f}=await a.createDeploy({extract:s,hookScript:i.hookScript,extensionScripts:i.extensionScripts.length>0?i.extensionScripts:void 0});return await a.activate(l),{configPath:n,versionId:l,alreadyDeployed:f,channels:Object.keys(s.channels),hasHookScript:!!i.hookScript,extensionCount:i.extensionScripts.length}}async function ie(e){await new x(e.apiUrl,e.secret).setSecret(e.name,e.value)}import se from"path";var Ge="https://api.useportal.co";function ae(e,n){let t=se.relative(n,e);return t&&!t.startsWith("..")&&!se.isAbsolute(t)?t:e}function g(){return(process.env.PORTAL_API_URL||Ge).replace(/\/+$/,"")}function q(){let e=process.env.PORTAL_SECRET;if(!e)throw new Error("Set PORTAL_SECRET to your project's secret key (sk_...) before running this command.");return e}async function ce(e){if(e!=null&&e!=="")return e;if(process.stdin.isTTY){let t=await nt("Value: ");if(!t)throw new Error("No value entered.");return t}let n=await Ve();if(!n)throw new Error("No value provided. Pass --value <value> or pipe the value on stdin.");return n}async function Ve(){let e=[];for await(let n of process.stdin)e.push(n);return Buffer.concat(e).toString("utf8").trim()}var Ye=10,Xe=13,Qe=4,Ze=3,et=8,tt=127;function nt(e){return new Promise(n=>{let t=process.stdin;process.stdout.write(e),t.setRawMode?.(!0),t.resume(),t.setEncoding("utf8");let r="",o=s=>{for(let i of s){let a=i.charCodeAt(0);if(a===Ye||a===Xe||a===Qe){t.setRawMode?.(!1),t.pause(),t.removeListener("data",o),process.stdout.write(`
54
- `),n(r);return}a===Ze&&(t.setRawMode?.(!1),t.pause(),t.removeListener("data",o),process.stdout.write(`
55
- `),process.exit(1)),a===et||a===tt?r=r.slice(0,-1):r+=i}};t.on("data",o)})}async function le(e){let n=await v();return new u(e.apiUrl).createApiKey(n,e.environmentId,e.keyType)}import{Portal as rt}from"@portalsdk/core";import{WebSocket as ot}from"ws";typeof globalThis.WebSocket>"u"&&(globalThis.WebSocket=ot);function it(e){let n=new Date(e.timestamp).toISOString(),t=e.sender.anon?`anon:${e.sender.id}`:e.sender.username??e.sender.id,r=typeof e.content=="string"?e.content:JSON.stringify(e.content);return`[${n}] ${t}: ${r}`}function pe(e){let n=e.log??(s=>console.log(s)),t=e.info??(s=>console.error(s)),o=new rt({apiKey:e.apiKey,...e.apiUrl?{apiUrl:e.apiUrl}:{},...e.realtimeUrl?{realtimeUrl:e.realtimeUrl}:{}}).channel(e.channelId,{history:"none"});return new Promise(s=>{let i=!1,a=()=>{i||(i=!0,process.off("SIGINT",a),l(),f(),o.release(),s())},l=o.on("message",p=>{n(it(p))}),f=o.on("status",(p,w)=>{p==="ready"&&t(`Listening on ${e.channelId} (Ctrl-C to exit)\u2026`),p==="blocked"&&(t(`Connection blocked${w?`: ${w.message}`:""}.`),a())});process.on("SIGINT",a),o.acquire()})}var K='Origin must be a bare "scheme://host[:port]" \u2014 e.g. https://app.example.com (no path, no trailing slash).';function st(e){let n=e.trim().replace(/\/+$/,""),t;try{t=new URL(n)}catch{return{ok:!1,message:K}}return t.protocol!=="http:"&&t.protocol!=="https:"?{ok:!1,message:K}:t.origin!==n?{ok:!1,message:K}:{ok:!0,origin:n}}async function de(e){let n=st(e.origin);if(!n.ok)throw new Error(n.message);let t=await v();return new u(e.apiUrl).createAllowedOrigin(t,e.environmentId,n.origin)}async function ue(e){let n=await v();return new u(e.apiUrl).listAllowedOrigins(n,e.environmentId)}async function fe(e){let n=await v();await new u(e.apiUrl).deleteAllowedOrigin(n,e.originId)}async function ge(e){let n=await v(),t=new u(e.apiUrl),r=await t.fetchIdentity(n),{organizations:o}=r;if(o.length===0)throw new Error("Your account has no organization yet \u2014 create one in the dashboard first.");if(o.length>1){let s=o.map(i=>i.name).join(", ");throw new Error(`Your account belongs to multiple organizations (${s}). portal projects create only supports a single organization right now.`)}return t.createProject(n,o[0].id,e.name)}var lt=at(import.meta.url),pt=lt("../package.json").version,m=new ct;m.name("portal").description("Sign in, deploy your Portal configuration, and manage secrets.").addHelpText("after",`
135
+ `}function ie(e){return Object.values(e.channels??{}).some(n=>n.authz!=null||(n.onPublish?.length??0)>0||(n.onDisconnect?.length??0)>0||n.notify!=null)}async function q(e,n,t){let r=C.dirname(n),o;if(ie(e)){let i=await Fe(n);o={name:He,content:i}}let s=[];for(let[i,a]of t)for(let l of a){let f=await Je(l.path,r);s.push({key:i,handle:l.handle,name:Ve(i,l.handle),durableObjectClass:N,content:f})}return Be(s),{hookScript:o,extensionScripts:s}}function Be(e){let n=new Map;for(let t of e){let r=n.get(t.name);if(r)throw new y(`Extensions "${r.handle}" (channel "${r.key}") and "${t.handle}" (channel "${t.key}") both map to the upload name "${t.name}". Rename a handle or channel so they differ by more than punctuation.`);n.set(t.name,t)}}function M(e,n){for(let t of n){let r=e.channels[t.key]?.extensions[t.handle];r&&(r.script=t.name)}}async function Fe(e){return se("portal-cli-hooks-","hooks.mjs",oe(e),C.dirname(e))}async function Je(e,n){return se("portal-cli-ext-","extension.mjs",re(e),n)}async function se(e,n,t,r){let o=await Me(C.join(Ke(),e)),s=C.join(o,n);try{return await ze(s,t,"utf8"),await Ge(s,r)}finally{await _e(o,{recursive:!0,force:!0})}}async function Ge(e,n){return(await We.build({entryPoints:[e],bundle:!0,write:!1,format:"esm",target:"es2022",platform:"neutral",logLevel:"silent",absWorkingDir:n})).outputFiles[0].text}function Ve(e,n){return`ext-${ne(e)}-${ne(n)}`}function ne(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}import{existsSync as Ye}from"fs";import Xe from"path";var P=class extends Error{};async function _(e,n){let t=new Map;for(let[r,o]of Object.entries(e.channels??{})){let s=o.extensions;if(!s||Object.keys(s).length===0)continue;let i=[];for(let[a,l]of Object.entries(s)){let f=Xe.resolve(n,l);if(!Ye(f))throw new P(`Channel "${r}" attaches extension "${a}" from "${l}", but that file does not exist.`);let p=await I(f),w=Qe(p.default);if(!w)throw new P(`Extension "${a}" (${l}) must \`export default defineExtension(...)\` with a static manifest.`);i.push({handle:a,source:l,namespace:w.namespace,transport:w.transport,path:f})}t.set(r,i)}return t}function Qe(e){if(typeof e!="function"&&typeof e!="object")return null;let n=e?.manifest;if(n==null||typeof n!="object")return null;let{namespace:t,transport:r}=n;return typeof t!="string"||!t.endsWith(".")||r!=="ws"&&r!=="http"?null:{namespace:t,transport:r}}async function ae(e){let n=ee(e.cwd,e.configPath),t=await te(n),r=Ze.dirname(n),o=await _(t,r);D(t,o);let s=R(t,o),i=await q(t,n,o);M(s,i.extensionScripts);let a=new v(e.apiUrl,e.secret),{versionId:l,alreadyDeployed:f}=await a.createDeploy({extract:s,hookScript:i.hookScript,extensionScripts:i.extensionScripts.length>0?i.extensionScripts:void 0});return await a.activate(l),{configPath:n,versionId:l,alreadyDeployed:f,channels:Object.keys(s.channels),hasHookScript:!!i.hookScript,extensionCount:i.extensionScripts.length}}async function ce(e){await new v(e.apiUrl,e.secret).setSecret(e.name,e.value)}import le from"path";var et="https://api.useportal.co";function pe(e,n){let t=le.relative(n,e);return t&&!t.startsWith("..")&&!le.isAbsolute(t)?t:e}function g(){return(process.env.PORTAL_API_URL||et).replace(/\/+$/,"")}function z(){let e=process.env.PORTAL_SECRET;if(!e)throw new Error("Set PORTAL_SECRET to your project's secret key (sk_...) before running this command.");return e}async function de(e){if(e!=null&&e!=="")return e;if(process.stdin.isTTY){let t=await ct("Value: ");if(!t)throw new Error("No value entered.");return t}let n=await tt();if(!n)throw new Error("No value provided. Pass --value <value> or pipe the value on stdin.");return n}async function tt(){let e=[];for await(let n of process.stdin)e.push(n);return Buffer.concat(e).toString("utf8").trim()}var nt=10,rt=13,ot=4,it=3,st=8,at=127;function ct(e){return new Promise(n=>{let t=process.stdin;process.stdout.write(e),t.setRawMode?.(!0),t.resume(),t.setEncoding("utf8");let r="",o=s=>{for(let i of s){let a=i.charCodeAt(0);if(a===nt||a===rt||a===ot){t.setRawMode?.(!1),t.pause(),t.removeListener("data",o),process.stdout.write(`
136
+ `),n(r);return}a===it&&(t.setRawMode?.(!1),t.pause(),t.removeListener("data",o),process.stdout.write(`
137
+ `),process.exit(1)),a===st||a===at?r=r.slice(0,-1):r+=i}};t.on("data",o)})}async function ue(e){let n=await x();return new u(e.apiUrl).createApiKey(n,e.environmentId,e.keyType)}import{Portal as lt}from"@portalsdk/core";import{WebSocket as pt}from"ws";typeof globalThis.WebSocket>"u"&&(globalThis.WebSocket=pt);function dt(e){let n=new Date(e.timestamp).toISOString(),t=e.sender.anon?`anon:${e.sender.id}`:e.sender.username??e.sender.id,r=typeof e.content=="string"?e.content:JSON.stringify(e.content);return`[${n}] ${t}: ${r}`}function fe(e){let n=e.log??(s=>console.log(s)),t=e.info??(s=>console.error(s)),o=new lt({apiKey:e.apiKey,...e.apiUrl?{apiUrl:e.apiUrl}:{},...e.realtimeUrl?{realtimeUrl:e.realtimeUrl}:{}}).channel(e.channelId,{history:"none"});return new Promise(s=>{let i=!1,a=()=>{i||(i=!0,process.off("SIGINT",a),l(),f(),o.release(),s())},l=o.on("message",p=>{n(dt(p))}),f=o.on("status",(p,w)=>{p==="ready"&&t(`Listening on ${e.channelId} (Ctrl-C to exit)\u2026`),p==="blocked"&&(t(`Connection blocked${w?`: ${w.message}`:""}.`),a())});process.on("SIGINT",a),o.acquire()})}var K='Origin must be a bare "scheme://host[:port]" \u2014 e.g. https://app.example.com (no path, no trailing slash).';function ut(e){let n=e.trim().replace(/\/+$/,""),t;try{t=new URL(n)}catch{return{ok:!1,message:K}}return t.protocol!=="http:"&&t.protocol!=="https:"?{ok:!1,message:K}:t.origin!==n?{ok:!1,message:K}:{ok:!0,origin:n}}async function ge(e){let n=ut(e.origin);if(!n.ok)throw new Error(n.message);let t=await x();return new u(e.apiUrl).createAllowedOrigin(t,e.environmentId,n.origin)}async function me(e){let n=await x();return new u(e.apiUrl).listAllowedOrigins(n,e.environmentId)}async function he(e){let n=await x();await new u(e.apiUrl).deleteAllowedOrigin(n,e.originId)}async function ye(e){let n=await x(),t=new u(e.apiUrl),r=await t.fetchIdentity(n),{organizations:o}=r;if(o.length===0)throw new Error("Your account has no organization yet \u2014 create one in the dashboard first.");if(o.length>1){let s=o.map(i=>i.name).join(", ");throw new Error(`Your account belongs to multiple organizations (${s}). portal projects create only supports a single organization right now.`)}return t.createProject(n,o[0].id,e.name)}var mt=ft(import.meta.url),ht=mt("../package.json").version,m=new gt;m.name("portal").description("Sign in, deploy your Portal configuration, and manage secrets.").addHelpText("after",`
56
138
  Authentication:
57
139
  login signs you in as a user (for account-level commands)
58
140
  PORTAL_SECRET your project's secret key (sk_...), used by deploy and secrets
59
- `).version(pt);m.command("login").description("Sign in to Portal.").option("--name <name>","Label for this machine (default: its hostname)").action(async e=>{await h(async()=>{let n=await J({apiUrl:g(),name:e.name}),t=n.identity?he(n.identity):null;console.log(`${c.green("\u2713")} Signed in${t?` as ${c.bold(t)}`:""}.`)})});m.command("whoami").description("Show who you are signed in as.").action(async()=>{await h(async()=>{let e=await G({apiUrl:g()});if(!e.loggedIn){console.log("Not signed in. Run: portal login");return}console.log(he(e.identity));let{organizations:n}=e.identity;n.length>0&&console.log(c.dim(` ${n.map(t=>t.name).join(", ")}`))})});m.command("logout").description("Sign out of Portal on this machine.").option("--all","Revoke every CLI session for your account, not just this machine").action(async e=>{await h(async()=>{let n=await V({apiUrl:g(),all:e.all});if(!n.wasLoggedIn){console.log("You were not signed in.");return}if(n.revokedCount!==void 0){let t=n.revokedCount;console.log(`${c.green("\u2713")} Signed out. Revoked ${t} CLI session${t===1?"":"s"}.`);return}console.log(`${c.green("\u2713")} Signed out.`)})});m.command("deploy").description("Deploy your portal.config.ts.").option("-c, --config <path>","Path to your config file (default: portal.config.ts)").action(async e=>{await h(async()=>{let n=q(),t=await oe({cwd:process.cwd(),configPath:e.config,apiUrl:g(),secret:n});gt(t)})});var dt=m.command("secrets").description("Manage your project secrets.");dt.command("set").argument("<name>",'Secret name, referenced in your config with env("NAME")').description("Set a project secret.").option("--value <value>","The secret value (otherwise read from stdin)").action(async(e,n)=>{await h(async()=>{let t=q(),r=await ce(n.value);await ie({name:e,value:r,apiUrl:g(),secret:t}),console.log(`${c.green("\u2713")} Secret ${c.bold(e)} set.`)})});var ut=m.command("projects").description("Manage your Portal projects.");ut.command("create").argument("<name>","Project name").description("Create a project (auto-picks your organization) and its two environments.").action(async e=>{await h(async()=>{let n=await ge({apiUrl:g(),name:e});console.log(`${c.green("\u2713")} Created project ${c.bold(n.project.name)}`),console.log(` Project: ${n.project.id}`);for(let t of n.environments)console.log(` ${mt(t.type)}: ${t.id}`)})});var ft=m.command("keys").description("Manage environment API keys.");ft.command("create").description("Create an API key for an environment. The value is printed once.").requiredOption("--env <envId>","Environment id").requiredOption("--type <type>","public|secret").action(async e=>{await h(async()=>{if(e.type!=="public"&&e.type!=="secret")throw new Error("--type must be public or secret.");let n=await le({apiUrl:g(),environmentId:e.env,keyType:e.type});console.log(`${c.green("\u2713")} Created ${n.keyType} key.`),console.log(` ${n.value}`),console.log(c.dim(" This value is shown once \u2014 store it now."))})});var _=m.command("origins").description("Manage an environment's allowed browser origins.");_.command("add").argument("<origin>","Browser origin, e.g. https://app.example.com").description("Register an origin so browsers on it can use a pk_ from that environment.").requiredOption("--env <envId>","Environment id").action(async(e,n)=>{await h(async()=>{let t=await de({apiUrl:g(),environmentId:n.env,origin:e});console.log(`${c.green("\u2713")} Registered ${c.bold(t.origin)}`),console.log(c.dim(" localhost / 127.0.0.1 never need registering \u2014 loopback is always allowed."))})});_.command("list").description("List an environment's registered origins.").requiredOption("--env <envId>","Environment id").action(async e=>{await h(async()=>{let n=await ue({apiUrl:g(),environmentId:e.env});if(n.length===0){console.log("No origins registered (loopback is always allowed regardless).");return}for(let t of n)console.log(`${t.id} ${t.origin}`)})});_.command("remove").argument("<id>","Origin id, from `portal origins list`").description("Remove a registered origin.").action(async e=>{await h(async()=>{await fe({apiUrl:g(),originId:e}),console.log(`${c.green("\u2713")} Removed.`)})});m.command("listen").argument("<channelId>","Channel id to listen on").description("Connect anonymously and print every incoming message as one line.").requiredOption("--key <key>","Publishable key (pk_...)").action(async(e,n)=>{await h(async()=>{await pe({apiKey:n.key,channelId:e})})});m.parseAsync(process.argv);async function h(e){try{await e()}catch(n){console.error(`${c.red("Error:")} ${n instanceof Error?n.message:String(n)}`),process.exitCode=1}}function gt(e){e.alreadyDeployed?console.log(`${c.green("\u2713")} Already up to date \u2014 version ${c.bold(e.versionId)}`):(console.log(`${c.green("\u2713")} Deployed ${c.bold(ae(e.configPath,process.cwd()))}`),console.log(` Version ${e.versionId}`));let{channels:n}=e;console.log(` ${n.length} channel ${me(n.length,"override","overrides")}: ${n.join(", ")}`);let t=[];e.hasHookScript&&t.push("hooks"),e.extensionCount>0&&t.push(`${e.extensionCount} ${me(e.extensionCount,"extension","extensions")}`),t.length>0&&console.log(` Uploaded: ${t.join(", ")}`),console.log(c.dim(" Channels with active connections keep their current configuration until they restart; new connections use this version right away."))}function me(e,n,t){return e===1?n:t}function mt(e){return e.charAt(0).toUpperCase()+e.slice(1)}function he(e){let{firstName:n,lastName:t,email:r}=e.user,o=[n,t].filter(Boolean).join(" ").trim();return o?`${o} (${r})`:r}
141
+ `).version(ht);m.command("login").description("Sign in to Portal.").option("--name <name>","Label for this machine (default: its hostname)").action(async e=>{await h(async()=>{let n=await V({apiUrl:g(),name:e.name}),t=n.identity?xe(n.identity):null;console.log(`${c.green("\u2713")} Signed in${t?` as ${c.bold(t)}`:""}.`)})});m.command("whoami").description("Show who you are signed in as.").action(async()=>{await h(async()=>{let e=await Y({apiUrl:g()});if(!e.loggedIn){console.log("Not signed in. Run: portal login");return}console.log(xe(e.identity));let{organizations:n}=e.identity;n.length>0&&console.log(c.dim(` ${n.map(t=>t.name).join(", ")}`))})});m.command("logout").description("Sign out of Portal on this machine.").option("--all","Revoke every CLI session for your account, not just this machine").action(async e=>{await h(async()=>{let n=await X({apiUrl:g(),all:e.all});if(!n.wasLoggedIn){console.log("You were not signed in.");return}if(n.revokedCount!==void 0){let t=n.revokedCount;console.log(`${c.green("\u2713")} Signed out. Revoked ${t} CLI session${t===1?"":"s"}.`);return}console.log(`${c.green("\u2713")} Signed out.`)})});m.command("deploy").description("Deploy your portal.config.ts.").option("-c, --config <path>","Path to your config file (default: portal.config.ts)").action(async e=>{await h(async()=>{let n=z(),t=await ae({cwd:process.cwd(),configPath:e.config,apiUrl:g(),secret:n});vt(t)})});var yt=m.command("secrets").description("Manage your project secrets.");yt.command("set").argument("<name>",'Secret name, referenced in your config with env("NAME")').description("Set a project secret.").option("--value <value>","The secret value (otherwise read from stdin)").action(async(e,n)=>{await h(async()=>{let t=z(),r=await de(n.value);await ce({name:e,value:r,apiUrl:g(),secret:t}),console.log(`${c.green("\u2713")} Secret ${c.bold(e)} set.`)})});var wt=m.command("projects").description("Manage your Portal projects.");wt.command("create").argument("<name>","Project name").description("Create a project (auto-picks your organization) and its two environments.").action(async e=>{await h(async()=>{let n=await ye({apiUrl:g(),name:e});console.log(`${c.green("\u2713")} Created project ${c.bold(n.project.name)}`),console.log(` Project: ${n.project.id}`);for(let t of n.environments)console.log(` ${kt(t.type)}: ${t.id}`)})});var xt=m.command("keys").description("Manage environment API keys.");xt.command("create").description("Create an API key for an environment. The value is printed once.").requiredOption("--env <envId>","Environment id").requiredOption("--type <type>","public|secret").action(async e=>{await h(async()=>{if(e.type!=="public"&&e.type!=="secret")throw new Error("--type must be public or secret.");let n=await ue({apiUrl:g(),environmentId:e.env,keyType:e.type});console.log(`${c.green("\u2713")} Created ${n.keyType} key.`),console.log(` ${n.value}`),console.log(c.dim(" This value is shown once \u2014 store it now."))})});var W=m.command("origins").description("Manage an environment's allowed browser origins.");W.command("add").argument("<origin>","Browser origin, e.g. https://app.example.com").description("Register an origin so browsers on it can use a pk_ from that environment.").requiredOption("--env <envId>","Environment id").action(async(e,n)=>{await h(async()=>{let t=await ge({apiUrl:g(),environmentId:n.env,origin:e});console.log(`${c.green("\u2713")} Registered ${c.bold(t.origin)}`),console.log(c.dim(" localhost / 127.0.0.1 never need registering \u2014 loopback is always allowed."))})});W.command("list").description("List an environment's registered origins.").requiredOption("--env <envId>","Environment id").action(async e=>{await h(async()=>{let n=await me({apiUrl:g(),environmentId:e.env});if(n.length===0){console.log("No origins registered (loopback is always allowed regardless).");return}for(let t of n)console.log(`${t.id} ${t.origin}`)})});W.command("remove").argument("<id>","Origin id, from `portal origins list`").description("Remove a registered origin.").action(async e=>{await h(async()=>{await he({apiUrl:g(),originId:e}),console.log(`${c.green("\u2713")} Removed.`)})});m.command("listen").argument("<channelId>","Channel id to listen on").description("Connect anonymously and print every incoming message as one line.").requiredOption("--key <key>","Publishable key (pk_...)").action(async(e,n)=>{await h(async()=>{await fe({apiKey:n.key,channelId:e})})});m.parseAsync(process.argv);async function h(e){try{await e()}catch(n){console.error(`${c.red("Error:")} ${n instanceof Error?n.message:String(n)}`),process.exitCode=1}}function vt(e){e.alreadyDeployed?console.log(`${c.green("\u2713")} Already up to date \u2014 version ${c.bold(e.versionId)}`):(console.log(`${c.green("\u2713")} Deployed ${c.bold(pe(e.configPath,process.cwd()))}`),console.log(` Version ${e.versionId}`));let{channels:n}=e;console.log(` ${n.length} channel ${we(n.length,"override","overrides")}: ${n.join(", ")}`);let t=[];e.hasHookScript&&t.push("hooks"),e.extensionCount>0&&t.push(`${e.extensionCount} ${we(e.extensionCount,"extension","extensions")}`),t.length>0&&console.log(` Uploaded: ${t.join(", ")}`),console.log(c.dim(" Channels with active connections keep their current configuration until they restart; new connections use this version right away."))}function we(e,n,t){return e===1?n:t}function kt(e){return e.charAt(0).toUpperCase()+e.slice(1)}function xe(e){let{firstName:n,lastName:t,email:r}=e.user,o=[n,t].filter(Boolean).join(" ").trim();return o?`${o} (${r})`:r}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portalsdk/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "The Portal command line: deploy your configuration and manage secrets.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,8 @@
36
36
  "tsx": "^4.7.1",
37
37
  "typescript": "^5.8.3",
38
38
  "vitest": "^3.2.4",
39
- "@portalsdk/config": "0.1.2"
39
+ "@portalsdk/extension-protocol": "^0.1.0",
40
+ "@portalsdk/config": "0.1.4"
40
41
  },
41
42
  "scripts": {
42
43
  "build": "tsup",