palbase 0.11.0 → 0.11.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.
- package/dist/ai/index.js +2 -2
- package/dist/backend/engine.js +2 -2
- package/dist/backend/env.js +1 -1
- package/dist/backend/index.js +3 -3
- package/dist/backend/stack.js +1 -1
- package/dist/backend/test.js +2 -2
- package/dist/chunks/{WYUC5N2T.js → 26UTOC32.js} +1 -1
- package/dist/chunks/{52DZTUF7.js → 3453XVQL.js} +2 -2
- package/dist/chunks/{5D7BAVO5.js → 3VY3ZZCI.js} +1 -1
- package/dist/chunks/{NHNXW6EK.js → 43VM626B.js} +1 -1
- package/dist/chunks/{KXY2SF4N.js → 7WIMZTYB.js} +2 -2
- package/dist/chunks/{D6RL5OCI.js → AFFE3Q5O.js} +2 -2
- package/dist/chunks/{2CQIOY2Z.js → BUAGC2PT.js} +1 -1
- package/dist/chunks/{2MS22NJC.js → BUYFMLTF.js} +2 -2
- package/dist/chunks/{S4GBHZOR.js → EEWP43KI.js} +1 -1
- package/dist/chunks/{476DZPIV.js → EWBRYA7P.js} +2 -2
- package/dist/chunks/{LJ3VOTHH.js → FVLHT7UQ.js} +2 -2
- package/dist/chunks/{7JE4KOPN.js → KNPQNBC2.js} +2 -2
- package/dist/chunks/{QX7E6TNL.js → KW6ODEYS.js} +1 -1
- package/dist/chunks/{XBT3QJRS.js → LYEEW4UG.js} +2 -2
- package/dist/chunks/{AEAFASN2.js → NGE3D2K4.js} +2 -2
- package/dist/chunks/{GCVVMTWI.js → Q3S76MYJ.js} +2 -2
- package/dist/chunks/{BAOQMZPI.js → R3EIGCWV.js} +2 -2
- package/dist/chunks/{GUFHH4DP.js → S2KRJF6J.js} +2 -2
- package/dist/chunks/{63TWL4SB.js → V2FEPVVD.js} +2 -2
- package/dist/chunks/{CQBT2Z2T.js → WLAO2QSR.js} +2 -2
- package/dist/chunks/{DAYQLOBB.js → ZZG7ICDG.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/refuse/ai.js +1 -1
- package/dist/refuse/backend.js +1 -1
- package/dist/refuse/server.js +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/testing.js +2 -2
- package/dist/web/index.js +2 -2
- package/dist/web/message.js +2 -2
- package/dist/web/palbe.js +2 -2
- package/dist/web/paywall.js +2 -2
- package/dist/web/push-sw.js +1 -1
- package/dist/web/ssr.js +2 -2
- package/dist/web/vite.js +2 -2
- package/package.json +2 -2
- package/types/internal/src/modules/purchases.d.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/*! palbase 0.11.
|
|
2
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
3
3
|
var lm=Object.defineProperty;var We=(e,t)=>{for(var n in t)lm(e,n,{get:t[n],enumerable:!0})};import{parseArgs as Fm}from"node:util";var pm=Symbol.for("palbase.data"),dm=globalThis,ao=dm[pm]??=new WeakSet;function cm(e){return e instanceof Date||ArrayBuffer.isView(e)||e instanceof ArrayBuffer}function Os(e){let t=[e];for(;t.length>0;){let n=t.pop();if(!(typeof n!="object"||n===null||ao.has(n)||cm(n))){ao.add(n);for(let s of Array.isArray(n)?n:Object.values(n))t.push(s)}}return e}var um="X-PoW-Challenge-ID",hm="X-PoW-Nonce";function ro(e){if(typeof e!="object"||e===null)return null;let t=e;if(t.error!=="pow_required"||typeof t.challenge!="object"||t.challenge===null)return null;let{id:n,prefix:s,difficulty:a}=t.challenge;return typeof n!="string"||typeof s!="string"||typeof a!="number"||!Number.isInteger(a)||a<0?null:{id:n,prefix:s,difficulty:a}}function gm(e){return 8*2**e}var cn;async function mm(){if(cn)return cn;let e=globalThis;if(e.process?.versions?.node!==void 0||e.process?.versions?.bun!==void 0)try{let s=(await import("node:crypto")).createHash;if(typeof s=="function")return cn=a=>new Uint8Array(s("sha256").update(a).digest()),cn}catch{}let t=new TextEncoder;return cn=async n=>new Uint8Array(await crypto.subtle.digest("SHA-256",t.encode(n))),cn}function fm(e){let t=0;for(let n of e){if(n===0){t+=8;continue}return t+Math.clz32(n)-24}return t}var ym=4096,bm=typeof globalThis.setImmediate=="function"?()=>new Promise(e=>globalThis.setImmediate(e)):()=>new Promise(e=>setTimeout(e,0));async function io(e,t={}){if(e.difficulty>24)throw new Error(`proof-of-work: difficulty ${e.difficulty} is above auth's ceiling of 24`);let n=await mm(),s=gm(e.difficulty),a=Date.now()+(t.timeLimitMs??12e4);for(let r=0;r<s;r++){if(r%ym===0&&r>0){if(await bm(),t.signal?.aborted)throw t.signal.reason??new DOMException("proof-of-work aborted","AbortError");if(Date.now()>a)throw new Error(`proof-of-work: gave up on difficulty ${e.difficulty} after ${r} attempts`)}let i=n(e.prefix+r),o=i instanceof Uint8Array?i:await i;if(fm(o)>=e.difficulty)return{[um]:e.id,[hm]:String(r)}}throw new Error(`proof-of-work: no nonce for difficulty ${e.difficulty} within ${s} attempts`)}var I=class extends Error{status;code;requestId;retryAfter;body;op;get isNetworkError(){return this.code==="network_error"}constructor(t){super(t.message,t.cause===void 0?void 0:{cause:t.cause}),this.name="PalbaseError",this.op=t.op,this.status=t.status,this.code=t.code,this.requestId=t.requestId,this.retryAfter=t.retryAfter,this.body=t.body}};function u(e,t,n){let s=String(n);if(s===""||s==="."||s==="..")throw new I({op:e,status:0,code:"invalid_path_parameter",message:`${e}: ${t} cannot be ${s===""?"empty":JSON.stringify(s)}`});return encodeURIComponent(s)}var wm=new Set(["apikey","authorization"]),vm=64*1024;function nr(e){let t=xs(e.baseUrl),n=e.fetch??globalThis.fetch,s=e.solveProofOfWork??!0,a=async(r,i)=>{let o=Sm(t,r),l=new Headers;for(let[f,w]of Object.entries(r.headers??{}))w!=null&&!wm.has(f.toLowerCase())&&l.set(f,String(w));for(let[f,w]of Object.entries(i))l.set(f,w);await $m(r,e.credentials,l),l.has("accept")||l.set("accept","application/json");let d={method:r.method,headers:l,redirect:"manual",signal:r.signal,...r.keepalive?{keepalive:!0}:{}};r.body&&r.body.value!==void 0&&(l.set("content-type",r.body.contentType),d.body=Pm(r.body.kind,r.body.value),un(d.body)||(d.duplex="half"));let p;try{p=r.onUploadProgress&&e.xmlHttpRequest?await _m(e.xmlHttpRequest,r.op,o,d,r.onUploadProgress):await n(o,d)}catch(f){throw r.signal?.aborted||f instanceof I?f:new I({op:r.op,status:0,code:"network_error",message:`${r.op}: the request did not reach ${t.origin}`,cause:f})}let c=r.answers[p.status];if(c===void 0)throw p.status>=200&&p.status<300?(await p.body?.cancel(),new I({op:r.op,status:p.status,code:"undeclared_answer",message:`${r.op}: the server answered ${p.status}, which its contract does not declare; the request may have taken effect`})):await Rm(r.op,p);let h;switch(c){case"json":try{h=Os(await p.json())}catch(f){throw new I({op:r.op,status:p.status,code:"invalid_json",message:`${r.op}: the answer is not JSON`,cause:f})}break;case"raw":h=p;break;default:await p.body?.cancel(),h=void 0}return{status:p.status,data:h,headers:p.headers}};return{async send(r){try{return await a(r,{})}catch(i){let o=s&&i instanceof I&&i.status===403?ro(i.body):null,l=r.body?.value;if(!o||r.body?.kind==="raw"&&!un(l))throw i;let d=i,p;try{p=await io(o,{signal:r.signal})}catch(c){throw r.signal?.aborted?c:new I({op:r.op,status:403,code:"pow_unsolved",message:`${r.op}: ${c instanceof Error?c.message:String(c)}`,requestId:d.requestId,body:d.body,cause:c})}return a(r,p)}}}}function _m(e,t,n,s,a){return new Promise((r,i)=>{let o=s.signal;if(o?.aborted)return i(o.reason);let l=new e,d=()=>l.abort();o?.addEventListener("abort",d,{once:!0});let p=c=>{o?.removeEventListener("abort",d),c()};l.open(s.method??"GET",n.href),l.responseType="arraybuffer",new Headers(s.headers).forEach((c,h)=>l.setRequestHeader(h,c)),l.upload&&(l.upload.onprogress=c=>{c.lengthComputable&&a(c.loaded,c.total)}),l.onload=()=>p(()=>{if(l.responseURL!==""&&l.responseURL!==n.href){i(new I({op:t,status:0,code:"undeclared_redirect",message:`${t}: the server redirected; the answer is another address's, and was not read`}));return}if(l.status<200||l.status>599)return i(new TypeError(`${t}: answered ${l.status}`));let c=l.status===204||l.status===205||l.status===304;r(new Response(c?null:l.response,{status:l.status,statusText:l.statusText,headers:km(l.getAllResponseHeaders())}))}),l.onerror=()=>p(()=>i(new TypeError(`${t}: the request failed`))),l.onabort=()=>p(()=>i(o?.reason??new DOMException("The request was aborted","AbortError"))),l.send(s.body)})}function km(e){let t=new Headers;for(let n of e.split(/\r?\n/)){let s=n.indexOf(":");if(!(s<=0))try{t.append(n.slice(0,s).trim(),n.slice(s+1).trim())}catch{}}return t}function xs(e){let t;try{t=new URL(e)}catch{throw new TypeError(`baseUrl ${JSON.stringify(e)} is not an absolute URL`)}let n=t.hostname==="localhost"||t.hostname.endsWith(".localhost")||t.hostname==="127.0.0.1"||t.hostname==="[::1]";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&n))throw new TypeError(`baseUrl must be https (http only for a loopback address): ${t.origin}`);if(t.username||t.password||t.search||t.hash)throw new TypeError("baseUrl carries no user, query or fragment");if(t.pathname!=="/"&&t.pathname!=="")throw new TypeError("baseUrl is the project origin alone, with no path");return t}function Sm(e,t){if(!t.path.startsWith("/")||t.path.startsWith("//"))throw new I({op:t.op,status:0,code:"invalid_path",message:`${t.op}: path ${JSON.stringify(t.path)} is not absolute`});let n=new URL(t.path+Am(t.query),e);if(n.origin!==e.origin)throw new I({op:t.op,status:0,code:"invalid_path",message:`${t.op}: path leaves the project's origin`});return n}async function $m(e,t,n){if(e.auth.length===0)return;let s=e.auth.some(i=>i.includes("userJWT"))?await t.accessToken?.():void 0,a=i=>{switch(i){case"ApiKeyAuth":return!!(t.publishableKey??t.secretKey);case"SecretKeyAuth":case"AdminAuth":return!!t.secretKey;case"userJWT":return!!s;default:return!1}},r=e.auth.filter(i=>i.every(a)).reduce((i,o)=>i===void 0||o.length>i.length?o:i,void 0);if(!r)throw new I({op:e.op,status:0,code:"missing_credential",message:`${e.op} needs one of: ${e.auth.map(i=>i.length===0?"nothing":i.join(" + ")).join(" | ")}`});for(let i of r)i==="ApiKeyAuth"?n.set("apikey",t.publishableKey??t.secretKey):i==="SecretKeyAuth"||i==="AdminAuth"?n.set("apikey",t.secretKey):i==="userJWT"&&n.set("authorization",`Bearer ${s}`)}function Pm(e,t){switch(e){case"json":return JSON.stringify(t);case"form":{let n=new URLSearchParams;for(let[s,a]of Object.entries(t))sr(n,s,a);return n}case"raw":return t}}function un(e){return!!(e==null||typeof e=="string"||e instanceof URLSearchParams||e instanceof Blob||e instanceof FormData||e instanceof ArrayBuffer||ArrayBuffer.isView(e))}function Am(e){if(!e)return"";let t=new URLSearchParams;for(let[s,a]of Object.entries(e))sr(t,s,a);let n=t.toString();return n===""?"":`?${n}`}function sr(e,t,n){if(n!=null){if(Array.isArray(n)){for(let s of n)sr(e,t,s);return}if(n instanceof Date){e.append(t,n.toISOString());return}e.append(t,typeof n=="object"?JSON.stringify(n):String(n))}}async function Rm(e,t){let n=await Tm(t,vm).catch(()=>""),s;try{s=n===""?void 0:JSON.parse(n)}catch{s=n}let a=Em(s)?s:void 0,r=Number(t.headers.get("retry-after")),i=t.status>=300&&t.status<400;return new I({op:e,status:t.status,code:a?.error??(i?"undeclared_redirect":`http_${t.status}`),message:a?.error_description??(i?`${e}: the server redirected (${t.status}); the request was not followed, its credentials stay here`:`${e}: ${t.status} ${t.statusText}`.trim()),requestId:a?.request_id??t.headers.get("x-request-id")??void 0,retryAfter:a?.retry_after??(Number.isFinite(r)&&r>0?r:void 0),body:s})}async function Tm(e,t){if(!e.body)return"";let n=e.body.getReader(),s=[],a=0;for(;a<t;){let{done:o,value:l}=await n.read();if(o)break;s.push(l),a+=l.byteLength}await n.cancel().catch(()=>{});let r=new Uint8Array(Math.min(a,t)),i=0;for(let o of s){let l=Math.min(o.byteLength,r.byteLength-i);if(r.set(o.subarray(0,l),i),i+=l,i>=r.byteLength)break}return new TextDecoder().decode(r)}function Em(e){return typeof e=="object"&&e!==null&&typeof e.error=="string"}var Is={project:{type:"string",short:"P",value:"project",description:"the project (name or id; org/project when the name is in two organizations)"},env:{type:"string",short:"E",value:"env",description:"the project's environment (slug or ref)"},json:{type:"boolean",description:"print the answer as JSON"},"non-interactive":{type:"boolean",description:"never ask: refuse what would need a question"},yes:{type:"boolean",short:"y",description:"confirm a change to a production environment without asking"},debug:{type:"boolean",description:"print each request the command sends"},help:{type:"boolean",short:"h",description:"show help"}};var y=class extends Error{exitCode;hint;code;constructor(t,n,s={}){super(n),this.name="CliError",this.code=t,this.hint=s.hint,this.exitCode=s.exitCode??1}};function g(e,t){return new y("usage",e,{hint:t,exitCode:2})}var Ms=class{io;json;constructor(t,n){this.io=t,this.json=n}result(t,n){this.json?this.io.stdout(`${JSON.stringify(t,null,2)}
|
|
4
4
|
`):n()}line(t=""){this.io.stdout(`${X(t)}
|
|
5
5
|
`)}note(t){this.io.stderr(`${X(t)}
|
|
@@ -9,7 +9,7 @@ var lm=Object.defineProperty;var We=(e,t)=>{for(var n in t)lm(e,n,{get:t[n],enum
|
|
|
9
9
|
`);let i;try{i=await this.io.fetch(t,{method:n.method,headers:{...n.headers,...a},body:n.body,signal:n.signal,...s?{}:{duplex:"half"},redirect:"manual"})}catch(l){throw n.signal?.aborted?l:new y("network_error",`${new URL(t).origin} could not be reached: ${l instanceof Error?l.message:String(l)}`)}if(this.debug&&this.io.stderr(`< ${i.status} ${i.headers.get("x-request-id")??""}
|
|
10
10
|
`),i.status!==503||!s||r>=Um||!Km.has(await ir(i)??""))return i;await i.body?.cancel();let o=Number(i.headers.get("retry-after")??"");await this.io.sleep((Number.isFinite(o)&&o>0?Math.min(o,30):1)*1e3)}}async exchange(t,n,s){let a=await this.token(),r=await this.send(t,n,s,a.value);if(r.status===401&&a.renewable){let i=await this.renew(await this.currentSession());r=await this.send(t,n,s,i.access_token)}return{res:r,renewable:a.renewable}}async answered(t,n,s,a){if(s.status===204)return;let r=await s.text(),i=r===""?void 0:zm(r);if(s.ok)return i;let o=i??{};throw s.status===401?o.error==="token_not_for_machines"?So(new URL(this.cloud.studioApi).host):a?hn():this.tokenRefused("Studio"):new I({op:`${t} ${n}`,status:s.status,code:o.error??`http_${s.status}`,message:o.error_description??`Studio answered ${s.status}`,requestId:o.request_id??s.headers.get("x-request-id")??void 0,body:i})}tokenRefused(t){return(this.io.env.PALBASE_TOKEN??"").startsWith("pbe_")?new y("token_refused",`${t} refused PALBASE_TOKEN, an environment's token: it ended (a few hours at most), it was revoked, or what minted it no longer holds`,{hint:"mint another: palbase env:tokens:create --scope <scope> (an admin), or with the environment's secret key (Studio's POST /environments/{ref}/tokens)"}):new y("token_refused",`${t} refused PALBASE_TOKEN: it was revoked, it ended, or the one who made it no longer holds its role`,{hint:"an admin makes a new one: palbase tokens:create <name>"})}async token(){let t=this.io.env.PALBASE_TOKEN;return t&&t!==""?{value:t,renewable:!1}:{value:(await this.currentSession()).access_token,renewable:!0}}async renew(t){let n=await wo(this.io,this.cloud,t);return await this.state.writeSession(n),this.session=n,n}async send(t,n,s,a){let r=`${this.cloud.studioApi}${Gm}${n}`;this.debug&&this.io.stderr(`> ${t} ${r}
|
|
11
11
|
`);try{let i=await this.io.fetch(r,{method:t,headers:{authorization:`Bearer ${a}`,accept:"application/json",...s===void 0?{}:{"content-type":"application/json"}},body:s===void 0?void 0:JSON.stringify(s),redirect:"manual"});return this.debug&&this.io.stderr(`< ${i.status} ${i.headers.get("x-request-id")??""}
|
|
12
|
-
`),i}catch(i){throw new y("network_error",`Studio (${this.cloud.studioApi}) could not be reached: ${i instanceof Error?i.message:String(i)}`)}}};function So(e){return new y("token_not_for_machines",`the platform's edge refused PALBASE_TOKEN at ${e}, taking it for a machine's address (token_not_for_machines): the token itself was not refused`,{hint:"nothing is wrong with the token: run this again in a few minutes, and tell Palbase if it lasts"})}async function ir(e){let t=await e.clone().json().catch(()=>null),n=t&&typeof t=="object"?t.error:void 0;return typeof n=="string"&&/^(console|token|edge)_/.test(n)?n:void 0}function zm(e){try{return JSON.parse(e)}catch{return e}}var $o={version:"0.11.
|
|
12
|
+
`),i}catch(i){throw new y("network_error",`Studio (${this.cloud.studioApi}) could not be reached: ${i instanceof Error?i.message:String(i)}`)}}};function So(e){return new y("token_not_for_machines",`the platform's edge refused PALBASE_TOKEN at ${e}, taking it for a machine's address (token_not_for_machines): the token itself was not refused`,{hint:"nothing is wrong with the token: run this again in a few minutes, and tell Palbase if it lasts"})}async function ir(e){let t=await e.clone().json().catch(()=>null),n=t&&typeof t=="object"?t.error:void 0;return typeof n=="string"&&/^(console|token|edge)_/.test(n)?n:void 0}function zm(e){try{return JSON.parse(e)}catch{return e}}var $o={version:"0.11.1"};var Gs=$o.version;async function Eo(e,t,n){let s=new Map(n.map(i=>[i.name,i])),a=e.includes("--json"),r=e.includes("--debug");try{let{name:i,at:o}=Wm(e);if(i===void 0)return e.includes("--version")?(t.stdout(`palbase ${Gs}
|
|
13
13
|
`),0):(t.stdout(To(n)),e.length===0||e.includes("--help")||e.includes("-h")?0:2);if(i==="help"){let k=e[o+1],R=k===void 0?void 0:s.get(k);if(k!==void 0&&!R)throw Po(k,n);return t.stdout(R?Ro(R):To(n)),0}let l=s.get(i);if(!l)throw Po(i,n);let d=[...e.slice(0,o),...e.slice(o+1)];if(d.includes("--help")||d.includes("-h"))return t.stdout(Ro(l)),0;let{flags:p,positionals:c}=Jm(l,d),h=l.args??{min:0,max:0};if(c.length<h.min)throw g(`${l.name} needs ${h.min===1?"an argument":`${h.min} arguments`}`,lr(l));if(c.length>h.max)throw g(`${l.name} takes ${h.max===0?"no argument":`at most ${h.max}`}: ${JSON.stringify(c.slice(h.max))} is left over`,lr(l));let f={project:p.project,env:p.env,json:p.json===!0,nonInteractive:p["non-interactive"]===!0,yes:p.yes===!0,debug:p.debug===!0},w=ho(t.env),v=new Ls(fo(t.configDir,w)),j={io:t,state:v,out:new Ms(t,f.json),global:f,args:c,flags:p,interactive:t.interactive&&!f.nonInteractive,cloud:w,studio:new qs(t,w,v,f.debug)};return await l.run(j)??0}catch(i){return Vm(i,t,a,r)}}function Wm(e){let t=new Set;for(let[n,s]of Object.entries(Is))s.type==="string"&&(t.add(`--${n}`),s.short&&t.add(`-${s.short}`));for(let n=0;n<e.length;n++){let s=e[n];if(s==="--")return{at:-1};if(t.has(s)){n++;continue}if(!s.startsWith("-"))return{name:s,at:n}}return{at:-1}}function Jm(e,t){let n={...Is,...e.flags??{}},s={};for(let[a,r]of Object.entries(n))s[a]={type:r.type,...r.short?{short:r.short}:{},...r.multiple?{multiple:!0}:{}};try{let{values:a,positionals:r}=Fm({args:t,options:s,strict:!0,allowPositionals:!0});return{flags:a,positionals:r}}catch(a){throw g(a instanceof Error?a.message.replace(/^.*?: /,""):String(a),`palbase help ${e.name}`)}}function Vm(e,t,n,s){let a="error",r,i,o=1,l;return e instanceof y?(a=e.code,r=e.message,i=e.hint,o=e.exitCode):e instanceof I?(a=e.code,r=e.message,l=e.requestId):r=e instanceof Error?e.message:String(e),n?t.stdout(`${JSON.stringify({error:a,message:r,...i?{hint:i}:{},...l?{request_id:l}:{}},null,2)}
|
|
14
14
|
`):(t.stderr(`error: ${X(r)}${l?` (request ${X(l)})`:""}
|
|
15
15
|
`),i&&t.stderr(` ${X(i)}
|
|
@@ -672,7 +672,7 @@ ${t}}`}var wt=e=>["",e.toUpperCase(),"-".repeat(e.length)],k0=[{title:"One packa
|
|
|
672
672
|
`?(o==="\r"&&i++,n.push(s),t.push(n),n=[],s="",r=!1):s+=o}return(r||s!==""||n.length>0)&&(n.push(s),t.push(n)),t}var a$=e=>/[",\r\n]/.test(e)||/^\s/.test(e)?`"${e.replace(/"/g,'""')}"`:e;function r$(e){return`${Kh(e).map(t=>t.map(n=>a$(n$.test(n)&&!s$.test(n)?`'${n}`:n)).join(",")).join(`
|
|
673
673
|
`)}
|
|
674
674
|
`}var Uh={flow:"total",snapshot:"last",ratio:"overall"};function zh(e){return(e.segments[0]?.points??[]).map(t=>t.period)}function i$(e,t,n){let s=t.segments[0];if(!s)return;let a=n?.measures??Object.keys(s.summary.measures).map(o=>({key:o,unit:t.unit,description:""})),r=[...s.points,{period:Uh[t.kind]??"summary",value:s.summary.value,measures:s.summary.measures}],i=[{header:"period",value:o=>`${o.period}${o.incomplete?" *":""}`},{header:"value",value:o=>$s(t.unit,o.value,t.currency)},...a.map(o=>({header:C(o.key),value:l=>o.key in l.measures?$s(o.unit,l.measures[o.key],t.currency):"\u2014"}))];e.out.table(r,i)}function o$(e,t){let s=zh(t).map((a,r)=>({period:a,values:t.segments.map(i=>i.points[r]?.value),incomplete:t.segments.some(i=>i.points[r]?.incomplete===!0)}));s.push({period:Uh[t.kind]??"summary",values:t.segments.map(a=>a.summary.value),incomplete:!1}),e.out.table(s,[{header:"period",value:a=>`${a.period}${a.incomplete?" *":""}`},...t.segments.map((a,r)=>({header:C(a.label??a.key),value:i=>i.values[r]===void 0?"\u2014":$s(t.unit,i.values[r],t.currency)}))])}function l$(e,t){for(let n of t.segments){let s=Math.max(0,...n.points.map(r=>r.ages?.length??0),n.summary.ages?.length??0),a=[...n.points.map(r=>({cohort:r.period,size:r.value,ages:r.ages??[],incomplete:r.incomplete})),{cohort:"all",size:n.summary.value,ages:n.summary.ages??[]}];t.segments.length>1&&e.out.line(`${C(n.label??n.key)}:`),e.out.table(a,[{header:"cohort",value:r=>`${r.cohort}${r.incomplete?" *":""}`},{header:"size",value:r=>$s(t.unit,r.size,t.currency)},...Array.from({length:s},(r,i)=>({header:`+${i}`,value:o=>{let l=o.ages.find(d=>d.age===i);return l?`${$s(t.age_unit,l.value,t.currency)}${l.incomplete?"*":""}`:""}}))])}}function p$(e){let t=[`${e.from} to ${e.to} (UTC) by ${e.resolution}`];return(e.unit==="currency"||e.age_unit==="currency"||e.revenue_type)&&t.push(`in ${e.currency}${e.revenue_type?`, ${Gi[e.revenue_type]??e.revenue_type}`:""}`),e.segment&&t.push(`by ${e.segment}`),e.filters.length>0&&t.push(`only ${e.filters.map(n=>`${n.dimension}:${n.value}`).join(", ")}`),e.window_days!==void 0&&t.push(`within ${e.window_days} day(s)`),e.cohort&&t.push(`cohorts of ${e.cohort==="new_paying_customers"?"new paying customers":"new customers"}`),t.push(`test purchases ${e.sandbox?"included":"left out"}`),`${e.name}: ${t.join(", ")}`}var d$={name:"purchases:charts:get",summary:"one purchase chart, period by period \u2014 a table of its periods and segments, or the module's CSV",usage:"<chart> [--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--resolution day|week|month] [--segment <dimension>] [--filter <dimension:value>]\u2026 [--revenue-type <type>] [--currency <code>] [--window-days <n>] [--cohort new_customers|new_paying_customers] [--sandbox] [--csv [--out <palbase/\u2026.csv>]]",details:["Days are UTC; from and to are both included, at most two years apart (by default the last 30 days, 12 weeks or 12 months). A period marked * is not over: its value may still change.","Money is in the reporting currency (purchases:charts:settings) at the European Central Bank's rate of the day it was paid; proceeds take off the store's commission and Stripe's fee, which the ledger does not hold: they are estimated, and so are taxes a store did not state.",`--csv prints the module's CSV (a row a segment and period) \u2014 on the terminal its control characters escaped \u2014; with --out it is written to that file, under ${G}/. A cell that would start a spreadsheet formula (=, +, -, @) begins with a quote.`],args:{min:1,max:1},flags:{from:{type:"string",value:"YYYY-MM-DD",description:"the first day"},to:{type:"string",value:"YYYY-MM-DD",description:"the last day, included (default today)"},resolution:{type:"string",value:"resolution",description:"day (default), week or month"},segment:{type:"string",value:"dimension",description:"split by a dimension (purchases:charts lists them)"},filter:{type:"string",multiple:!0,value:"dimension:value",description:"keep only this value, one each --filter: --filter store:app_store --filter country:TR"},"revenue-type":{type:"string",value:"type",description:"gross (default), net_of_taxes or proceeds"},currency:{type:"string",value:"code",description:"the currency to report money in (default: the reporting currency)"},"window-days":{type:"string",value:"n",description:"the conversion window or customer lifetime, in days from a customer's first day, 0 to 730 (default: to now)"},cohort:{type:"string",value:"cohort",description:"the cohort explorer's cohorts: new_customers (default) or new_paying_customers"},sandbox:{type:"boolean",description:"count the stores' test purchases too"},csv:{type:"boolean",description:"the module's CSV instead of a table"},out:{type:"string",value:"file",description:`with --csv, write it to this file, under ${G}/`}},scope:"environment",async run(e){let t=e.args[0];if(!/^[a-z_]+$/.test(t))throw g(`${JSON.stringify(t)} is not a chart's key`,"palbase purchases:charts lists them");for(let P of["from","to"]){let $=e.flags[P];if($!==void 0&&(typeof $!="string"||!WS.test($)))throw g(`--${P} is a day: 2026-10-01`)}let n=Ss(e,"resolution",VS),s=Ss(e,"revenue-type",qh),a=Ss(e,"cohort",HS),r=Wa(e,"currency"),i=le(e.flags["window-days"],"window-days");if(i!==void 0&&i>730)throw g("--window-days is 0 to 730");let o=t$(e.flags.filter),l=e.flags.segment;if(l!==void 0&&!/^[a-z][a-z_]{0,31}$/.test(l))throw g(`--segment ${l}: a dimension (purchases:charts lists them)`);let d=e.flags.csv===!0,p=e.flags.out;if(p!==void 0&&!d)throw g("--out writes the CSV: give --csv with it");if(d&&p===void 0&&e.global.json)throw g("--csv and --json are two forms of the answer: give one");let c=p!==void 0?Ie(e,"out",p,".csv",`${FS}/${t}.csv`):void 0,h=await b(e),f;try{f=await m(e,h,"GET","/purchases/charts")}catch(P){Ba(P,t)}let w=f.charts.find(P=>P.key===t);if(!w)throw new y("chart_not_found",`no chart ${JSON.stringify(t)}`,{hint:"palbase purchases:charts lists them"});let v=f.dimensions.map(P=>P.key);if((l!==void 0||o.length>0)&&!w.segmentable)throw g(`${t} is not segmented or filtered by a dimension`);for(let P of[...l!==void 0?[l]:[],...o.map($=>$.dimension)])if(!v.includes(P))throw g(`${P} is not a dimension the charts are cut by: ${v.map(C).join(", ")}`);let j=new URLSearchParams({...e.flags.from!==void 0?{from:e.flags.from}:{},...e.flags.to!==void 0?{to:e.flags.to}:{},...n?{resolution:n}:{},...e.flags.sandbox===!0?{sandbox:"true"}:{},...l?{segment:l}:{},...r?{currency:r}:{},...s?{revenue_type:s}:{},...i!==void 0?{window_days:String(i)}:{},...a?{cohort:a}:{}});for(let P of o)j.append("filter",`${P.dimension}:${P.value}`);let k=`/purchases/charts/${S(t)}${j.size>0?`?${j}`:""}`;if(d){let P;try{P=await m(e,h,"GET",k,void 0,{accept:"text/csv"})}catch(Y){Ba(Y,t)}if(typeof P!="string")throw new y("not_csv","the purchases module did not answer the chart as CSV");let $=r$(P);if(c===void 0){e.io.stdout(X($));return}let M=await pe(e.io.cwd,c);await US(BS(M),{recursive:!0}),await pe(e.io.cwd,c),await zS(M,$);let U=Math.max(0,Kh($).length-1);e.out.result({file:c,chart:t,rows:U},()=>e.out.line(`Wrote ${t} to ${c} (${U} row${U===1?"":"s"}).`));return}let R;try{R=await m(e,h,"GET",k)}catch(P){Ba(P,t)}e.out.result(R,()=>{e.out.line(C(p$(R))),R.segments.length===0||zh(R).length===0?e.out.line("Nothing in these days."):R.kind==="cohort"?(l$(e,R),e.out.line(R.chart==="subscription_retention"?"Ages are renewals: +0 is the first paid period; a value is the share that reached it.":`Ages are ${C(R.resolution)}s from each customer's first day; a value is realized LTV per customer.`)):R.segment===void 0&&R.segments.length===1?i$(e,R,w):o$(e,R),R.segments.some(P=>P.points.some($=>$.incomplete||$.ages?.some(M=>M.incomplete)))&&e.out.line("* not over yet: the value may still change."),R.estimated&&e.out.line("Estimated: the money holds what the ledger does not state \u2014 taxes a store did not, the stores' commission and Stripe's fee, a Google Play offer's period at its plan's price \u2014 in any revenue type, gross too."),R.chart==="active_customers"&&e.out.line("Customers are counted from the day the module began keeping the days they were seen: days before it count none."),Gh(e,R.unconverted),e.out.line(`Counted ${C(D(R.computed_at))}.`)})}};function c$(e){return`${Bh(e.stripe_fee_bps)} + ${Fa(e.stripe_fee_fixed_micros/1e6,e.stripe_fee_fixed_currency)}`}function Bh(e){return`${(e/100).toFixed(2).replace(/\.?0+$/,"")}%`}function u$(e){if(e===void 0)return;let t=Number(e);if(typeof e!="string"||!/^\d+(\.\d{1,2})?$/.test(e)||t>20)throw g("--stripe-percent is a share of a payment, 0 to 20, with at most two decimals: 2.9");return Math.round(t*100)}function h$(e){if(e===void 0)return;if(typeof e!="string"||!/^\d+(\.\d{1,6})?$/.test(e)||Number(e)>1e5)throw g("--stripe-fixed is an amount, 0 to 100000, with at most six decimals: 0.30");let[t,n=""]=e.split(".");return Number(t)*1e6+Number(n.padEnd(6,"0"))}function g$(e){if(e===void 0)return;let t=Number(e);if(typeof e!="string"||!/^\d+(\.\d{1,2})?$/.test(e)||t>30)throw g("--app-store-link-percent is a share of a web purchase, 0 to 30, with at most two decimals: 27");return Math.round(t*100)}function m$(e){if(e!==void 0){if(typeof e!="string"||!/^\d{1,2}$/.test(e)||Number(e)<1||Number(e)>90)throw g("--app-store-link-days is a number of days, 1 to 90: 7");return Number(e)}}var f$={name:"purchases:charts:settings",summary:"the charts' settings \u2014 the reporting currency, the stores' commission, Stripe's fee and the App Store's fee on web purchases linked out of the app \u2014 read, or changed with the flags given",usage:"[--currency <code>] [--commission standard|small_business] [--stripe-percent <n>] [--stripe-fixed <amount>] [--stripe-fixed-currency <code>] [--app-store-link-percent <n>] [--app-store-link-days <n>]",details:["Every amount is converted to the reporting currency at the European Central Bank's rate of the day it was paid; the ledger keeps what was paid as it was paid, so a change redraws every chart from its first day.","The commission is what proceeds take off App Store and Google Play payments: standard (30%; 15% of a subscription's renewals after its first paid year in the App Store, and of every Google Play subscription) or small_business (15%). Stripe's fee \u2014 its share and fixed part \u2014 is taken off web payments; the ledger does not hold it, so proceeds are estimated.","The App Store's link fee is what Apple takes of a web purchase bought through a link out of the iOS app in its United States storefront, within the days after the link: nothing today (App Review Guideline 3.1.1(a)), set here once Apple charges one."],flags:{currency:{type:"string",value:"code",description:"the reporting currency, one the European Central Bank published a rate for in the last ten days"},commission:{type:"string",value:"commission",description:"standard (30%) or small_business (15%)"},"stripe-percent":{type:"string",value:"n",description:"Stripe's share of a payment, in percent (2.9)"},"stripe-fixed":{type:"string",value:"amount",description:"Stripe's fixed fee of a payment (0.30)"},"stripe-fixed-currency":{type:"string",value:"code",description:"the currency of Stripe's fixed fee (USD)"},"app-store-link-percent":{type:"string",value:"n",description:"the App Store's share of a web purchase linked out of the iOS app, in percent (0)"},"app-store-link-days":{type:"string",value:"n",description:"for how many days after the link a purchase owes it (7)"}},scope:"environment",async run(e){let t={},n=Wa(e,"currency");n!==void 0&&(t.reporting_currency=n);let s=Ss(e,"commission",XS);s!==void 0&&(t.store_commission=s);let a=u$(e.flags["stripe-percent"]);a!==void 0&&(t.stripe_fee_bps=a);let r=h$(e.flags["stripe-fixed"]);r!==void 0&&(t.stripe_fee_fixed_micros=r);let i=Wa(e,"stripe-fixed-currency");i!==void 0&&(t.stripe_fee_fixed_currency=i);let o=g$(e.flags["app-store-link-percent"]);o!==void 0&&(t.app_store_link_fee_bps=o);let l=m$(e.flags["app-store-link-days"]);l!==void 0&&(t.app_store_link_attribution_days=l);let d=await b(e),p;Object.keys(t).length===0?p=await m(e,d,"GET","/purchases/settings"):p=await y$(e,d,t),e.out.result({reporting_currency:p.reporting_currency,store_commission:p.store_commission,stripe_fee_bps:p.stripe_fee_bps,stripe_fee_fixed_micros:p.stripe_fee_fixed_micros,stripe_fee_fixed_currency:p.stripe_fee_fixed_currency,app_store_link_fee_bps:p.app_store_link_fee_bps,app_store_link_attribution_days:p.app_store_link_attribution_days},()=>{e.out.table([["Reporting currency",p.reporting_currency],["Store commission",YS[p.store_commission]??p.store_commission],["Stripe's fee",c$(p)],["App Store's link fee",`${Bh(p.app_store_link_fee_bps)} within ${p.app_store_link_attribution_days} day${p.app_store_link_attribution_days===1?"":"s"} of the link`]],[{header:"setting",value:c=>c[0]},{header:"value",value:c=>c[1]}])})}};async function y$(e,t,n){let s=[n.reporting_currency&&`report money in ${n.reporting_currency}`,n.store_commission&&`take the ${n.store_commission==="small_business"?"Small Business (15%)":"standard (30%)"} commission off store payments`,(n.stripe_fee_bps!==void 0||n.stripe_fee_fixed_micros!==void 0||n.stripe_fee_fixed_currency)&&"change Stripe's fee",(n.app_store_link_fee_bps!==void 0||n.app_store_link_attribution_days!==void 0)&&"change the App Store's link fee"].filter(Boolean);return await _(e,t,`charts: ${s.join(", ")}`),m(e,t,"PUT","/purchases/settings",n)}var Fh=[QS,e$,d$,f$];import{X509Certificate as b$}from"node:crypto";import{mkdir as w$,readFile as Ui,stat as Jh,writeFile as Vh}from"node:fs/promises";import{basename as v$,dirname as _$,relative as k$,resolve as Va,sep as S$}from"node:path";var Ja={stripe:"Stripe",apple:"App Store",google:"Google Play"};function rn(e,t,n){let s=e.args[t];if(s===void 0||s.trim()==="")throw g(`${n} is required`);return s}function $$(e){return`"${e}"`}async function Hh(e){let t=be(await Se(e,e.flags.file,"the catalog"),"the catalog");if(typeof t!="object"||t===null||Array.isArray(t))throw g("the catalog is a JSON object: { base_locale, products, entitlements, credits, limits }");if("catalog"in t&&"revision"in t)throw g('the file is a saved revision (catalog:get --json): give its "catalog" alone, as catalog:get --out writes it');return t}function Xh(e,t){for(let n of t)e.out.note(` ${n.warning?"warning":"problem"} ${n.path}: ${n.message} (${n.code}${n.channel?`, ${n.channel}`:""})`)}function Yh(e,t,n){if(t instanceof I&&t.code==="invalid_catalog"){let s=(t.body??{}).problems??[];throw s.length>0&&(e.out.note(`The catalog breaks ${s.filter(a=>!a.warning).length} rule(s):`),Xh(e,s)),new y("invalid_catalog",`${n}: ${t.message}`,{hint:"fix each problem in the file (palbase purchases:catalog:validate checks it without saving)"})}throw t}function P$(e,t){if(t.revision===0){e.out.line("No catalog yet (revision 0)."),e.out.line("Write one and save it: palbase purchases:catalog:put --file catalog.json --revision 0");return}let n=t.catalog;e.out.line(`Catalog revision ${t.revision}${t.created_at?`, saved ${D(t.created_at)}`:""}${t.created_by?` by ${t.created_by}`:""}`),e.out.line(`base language: ${n.base_locale}`),e.out.table(n.products,[{header:"product",value:s=>s.key},{header:"type",value:s=>s.type},{header:"plans",value:s=>s.plans.map(a=>a.key).join(", ")},{header:"channels",value:s=>Object.keys(s.channels).sort().join(", ")},{header:"gives",value:s=>[...s.entitlements??[],...s.credits?[`${s.credits.amount} ${s.credits.currency}`]:[]].join(", ")}]),n.products.length===0&&e.out.line("No product."),n.entitlements.length>0&&e.out.line(`entitlements: ${n.entitlements.map(s=>s.key).join(", ")}`),n.credits.length>0&&e.out.line(`credits: ${n.credits.map(s=>s.key).join(", ")}`),n.limits.length>0&&e.out.line(`limits: ${n.limits.map(s=>s.key).join(", ")}`)}var A$={name:"purchases:catalog:get",summary:"the environment's catalog: its newest revision, the products it sells and what they give",usage:"[--out <catalog.json>]",details:["--out writes the catalog to a file to edit; catalog:put saves the edit with the revision it was edited from, which is printed."],flags:{out:{type:"string",value:"catalog.json",description:"write the catalog (the JSON catalog:put takes) to this file"}},scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/purchases/catalog"),s=e.flags.out;if(s!==void 0){await Vh(Va(e.io.cwd,s),`${JSON.stringify(n.catalog,null,2)}
|
|
675
|
-
`),e.out.result({file:s,revision:n.revision},()=>{e.out.line(`Wrote revision ${n.revision} of the catalog to ${s}.`),e.out.line(`Save an edit of it: palbase purchases:catalog:put --file ${s} --revision ${n.revision}`)});return}e.out.result(n,()=>P$(e,n))}},R$={name:"purchases:catalog:put",summary:"save a new revision of the whole catalog, on the revision it was edited from; nothing is written to a store",usage:"--file <catalog.json> --revision <n>",details:["The module checks it first: a catalog that breaks a rule is refused with every problem, and nothing is saved.","Of two people editing at once, the second is told \u2014 the catalog moved past the revision they edited \u2014 never overwritten.","The stores are brought to it by a plan: palbase purchases:catalog:plan."],flags:{file:{type:"string",value:"catalog.json",description:"the catalog, as catalog:get --out wrote it ('-' or none: stdin)"},revision:{type:"string",value:"n",description:"the revision it was edited from (0 for the first), as catalog:get printed it"}},scope:"environment",async run(e){let t=await b(e),n=le(e.flags.revision,"revision");if(n===void 0)throw g("--revision is the revision the catalog was edited from (0 for the first): of two edits at once, the second is told, not saved over the first");let s=await Hh(e);await _(e,t,`save the catalog (${s.products?.length??0} products) over revision ${n}`);let a;try{a=await m(e,t,"PUT","/purchases/catalog",s,{"if-match":$$(n),...x()})}catch(r){if(r instanceof I&&r.status===412)throw new y("catalog_changed",`the catalog is no longer at revision ${n}: someone saved a newer one since it was read \u2014 nothing was saved`,{hint:"read it again (palbase purchases:catalog:get --out <file>), make the edit on it, and save it with that revision"});Yh(e,r,"the catalog was not saved")}e.out.result(a,()=>{e.out.line(`Saved revision ${a.revision} of the catalog.`),e.out.line("Bring the stores to it: palbase purchases:catalog:plan")})}},T$={name:"purchases:catalog:validate",summary:"check a catalog without saving it: every problem, with its path and the store whose rule it breaks",usage:"--file <catalog.json>",flags:{file:{type:"string",value:"catalog.json",description:"the catalog ('-' or none: stdin)"}},scope:"environment",async run(e){let t=await b(e),n=await Hh(e),s;try{s=await m(e,t,"POST","/purchases/catalog/validate",n,x())}catch(a){Yh(e,a,"the catalog cannot be read")}if(e.out.result(s,()=>{s.problems.length>0&&Xh(e,s.problems),e.out.line(s.valid?`The catalog can be saved${s.problems.length>0?`, with ${s.problems.length} warning(s)`:""}.`:"The catalog cannot be saved as it is.")}),!s.valid)throw new y("invalid_catalog",`the catalog breaks ${s.problems.filter(a=>!a.warning).length} rule(s)`)}};function Ki(e,t,n){e.out.line(`${t}${n.store_env?` (${n.store_env})`:""}: ${n.steps.length===0?"nothing to write":`${n.steps.length} step(s)`}`);for(let s of n.steps)e.out.line(` ${s.op} ${s.product}${s.plan?`/${s.plan}`:""}${s.store_id?` (${s.store_id})`:""}${s.irreversible?" \u2014 permanent":""}${s.note?`: ${s.note}`:""}`);n.note&&e.out.line(` ${n.note}`)}function E$(e,t){if(e.out.line(`Plan ${t.id} for revision ${t.revision}: ${t.status}`),t.stripe?Ki(e,Ja.stripe,t.stripe):e.out.line(`${Ja.stripe}: no account connected (palbase purchases:stripe:grant connects one)`),Ki(e,Ja.apple,t.apple),Ki(e,Ja.google,t.google),t.problems.length>0){e.out.line("Warnings of the revision:");for(let r of t.problems)e.out.line(` ${r.path}: ${r.message}${r.channel?` (${r.channel})`:""}`)}t.status==="failed"&&t.error&&e.out.line(`Stripe answered: ${t.error}`);let n=t.apple_result;if(n){e.out.line(`App Store Connect (${n.bundle_id}): ${n.ok?"written":`not all written${n.error?`: ${n.error}`:""}`}`);for(let r of n.steps){let i=r.price?`, ${r.price.customer_price} ${r.price.currency}`:"",o=r.next_price?`, ${r.next_price.customer_price} ${r.next_price.currency} from ${r.next_price.from}`:"";e.out.line(` ${r.op} ${r.store_id}${r.state?`: ${r.state}`:""}${i}${o}${r.error?` \u2014 ${r.error}`:""}`)}}let s=t.google_result;if(s){e.out.line(`Google Play (${s.package_name}): ${s.pending?"waiting for the stores module":s.ok?"written":`not all written${s.error?`: ${s.error}`:""}`}`);for(let r of s.steps)e.out.line(` ${r.op} ${r.store_id}${r.state?`: ${r.state}`:""}${r.error?` \u2014 ${r.error}`:""}`)}let a=t.stripe?t.stripe.hash:t.apple.hash||t.google.hash;t.status==="planned"&&a&&(e.out.line(` hash: ${a}`),e.out.line(t.stripe?`The Stripe part is written only when confirmed${t.apple.steps.length>0?" (the App Store part goes with it, to the stores module)":""}:`:t.apple.hash?"The App Store part is written only when confirmed (the stores module writes it to App Store Connect):":"The Google Play part is written only when confirmed (the stores module writes it to Google Play):"),e.out.line(` palbase purchases:catalog:confirm ${t.id} --hash ${a}`))}var C$={name:"purchases:catalog:plan",summary:"plan bringing the stores to the catalog's newest revision, store by store, before anything is written",details:["A plan is confirmed with its Stripe part's hash (purchases:catalog:confirm) \u2014 with no Stripe account connected, its App Store part's; the App Store and Google Play parts are written by the stores module once their accounts are connected."],scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"POST","/purchases/catalog/plans",void 0,x());e.out.result(n,()=>E$(e,n))}},j$={name:"purchases:catalog:confirm",summary:"write a plan \u2014 its Stripe part now, its App Store part through the stores module: it names the hash of the plan that was shown, and is asked about first",usage:"<plan id> --hash <hash>",details:["What it writes is shown, then asked about \u2014 in every environment: a price written to Stripe is never changed or removed there, a product made in App Store Connect never goes away. -y confirms without asking."],args:{min:1,max:1},flags:{hash:{type:"string",value:"hash",description:"the hash the plan printed (its Stripe part's; with no Stripe account, its App Store part's)"}},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the plan's id"),s=e.flags.hash;if(s===void 0||s.trim()==="")throw g("--hash is the hash purchases:catalog:plan printed: a confirmation names the plan it saw");let a=await m(e,t,"GET",`/purchases/catalog/plans/${S(n)}`);if(a.status!=="planned")throw new y("plan_not_open",`the plan ${n} is ${a.status}: only a planned one is confirmed`,{hint:"make a new one: palbase purchases:catalog:plan"});let r=a.apple.steps.length;if(a.stripe)await _(e,t,`write ${a.stripe.steps.length} step(s) of revision ${a.revision} to Stripe${r>0?`, and ${r} to App Store Connect`:""}`,"It writes the project's Stripe account, where a price is never changed or removed");else{if(!a.apple.hash||r===0)throw new y("no_stripe_part",`the plan ${n} has no Stripe part (no Stripe account was connected when it was made) and no App Store part to confirm`);await _(e,t,`write ${r} step(s) of revision ${a.revision} to App Store Connect`,"It makes the products in App Store Connect, where a product id never goes away")}let i=await m(e,t,"POST",`/purchases/catalog/plans/${S(n)}/confirm`,{hash:s},x());if(e.out.result(i,()=>{e.out.line(`Plan ${i.id}: ${i.status}${i.applied_at?` (${D(i.applied_at)})`:""}.`),i.status==="failed"&&i.error&&e.out.line(`Stripe answered: ${i.error}`),r>0&&!i.apple_result&&e.out.line("The stores module is writing the App Store part to App Store Connect: palbase purchases:doctor says when it took it.")}),i.status==="failed")throw new y("plan_failed",`the plan ${i.id} failed: a new plan picks up where it stopped`)}},O$={name:"purchases:catalog:decline",summary:"decline a plan: its Stripe part is never written",usage:"<plan id>",args:{min:1,max:1},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the plan's id");await _(e,t,`decline the plan ${n}`);let s=await m(e,t,"POST",`/purchases/catalog/plans/${S(n)}/decline`,void 0,x());e.out.result(s,()=>e.out.line(`Declined the plan ${s.id}.`))}};function Wh(e){return e===null?"for life":D(e)}function x$(e,t){e.out.line(`Customer ${t.customer_id}${t.guest?" (guest)":""}: revision ${t.revision}, as of ${D(t.requested_at)}`);let n=Object.entries(t.entitlements).sort(([s],[a])=>s<a?-1:s>a?1:0);e.out.line(n.length===0?"No entitlement.":"Entitlements:"),e.out.table(n,[{header:"entitlement",value:([s])=>s},{header:"active",value:([,s])=>s.active?"yes":"no"},{header:"until",value:([,s])=>Wh(s.expires_at)},{header:"store",value:([,s])=>`${s.store}${s.is_sandbox?" (sandbox)":""}`},{header:"renews",value:([,s])=>s.will_renew?"yes":"no"},{header:"product",value:([,s])=>s.product??"\u2014"}]),t.subscriptions.length>0&&(e.out.line("Subscriptions:"),e.out.table(t.subscriptions,[{header:"product",value:s=>`${s.product??"\u2014"}${s.plan?`/${s.plan}`:""}`},{header:"store",value:s=>`${s.store}${s.is_sandbox?" (sandbox)":""}`},{header:"state",value:s=>s.state},{header:"until",value:s=>Wh(s.expires_at)},{header:"renews",value:s=>s.will_renew?"yes":"no"}])),t.purchases.length>0&&(e.out.line("Purchases:"),e.out.table(t.purchases,[{header:"product",value:s=>s.product??"\u2014"},{header:"store",value:s=>`${s.store}${s.is_sandbox?" (sandbox)":""}`},{header:"state",value:s=>s.state},{header:"bought",value:s=>D(s.purchased_at)}])),t.credits.length>0&&(e.out.line("Credits:"),e.out.table(t.credits,[{header:"credit",value:s=>s.currency},{header:"balance",value:s=>String(s.balance)},{header:"available",value:s=>String(s.available)},{header:"held",value:s=>String(s.reserved)},{header:"owed",value:s=>String(s.debt)},{header:"next expiry",value:s=>D(s.next_expiry)}])),t.limits.length>0&&(e.out.line("Limits:"),e.out.table(t.limits,[{header:"limit",value:s=>s.limit},{header:"used",value:s=>`${s.used} of ${s.max}`},{header:"held",value:s=>String(s.reserved)},{header:"left",value:s=>String(s.remaining)},{header:"resets",value:s=>D(s.resets_at)}]))}var I$={name:"purchases:customer:get",summary:"what a customer has, whatever store they bought in: entitlements, subscriptions, purchases, credits and limits",usage:"<customer id>",details:["The customer is the user's id; a guest merged into an account answers as the account."],args:{min:1,max:1},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the customer's id (the user's id)"),s=await m(e,t,"GET",`/purchases/customers/${S(n)}`);e.out.result(s,()=>x$(e,s))}};function M$(e){let t=e.flags.until,n=e.flags.for;if(t!==void 0&&n!==void 0)throw g("--until or --for, not both");if(t!==void 0){let s=Date.parse(t);if(!Number.isFinite(s))throw g(`--until ${t}: a date or time (2026-12-31, 2026-12-31T18:00:00Z)`);if(s<=e.io.now())throw g(`--until ${t} is not in the future`);return new Date(s).toISOString()}if(n!==void 0){let s=/^([1-9][0-9]{0,4})([hdw])$/.exec(n.trim());if(!s)throw g(`--for ${n}: a number of hours, days or weeks (12h, 30d, 2w)`);let a=Number(s[1])*{h:1,d:24,w:168}[s[2]];return new Date(e.io.now()+a*36e5).toISOString()}}var L$={name:"purchases:customer:grant",summary:"give a customer one of the catalog's entitlements, for a while or for life, saying why \u2014 a promotion, a support case, a tester",usage:"<customer id> <entitlement> --reason <why> [--for 30d | --until <date>]",details:["The reason is the record. Without --for or --until the grant is for life. It prints the grant's id: purchases:customer:revoke ends it.","A customer not seen before is made; nothing is charged, in any store."],args:{min:2,max:2},flags:{reason:{type:"string",value:"why",description:"why it is given (500 characters at most): the record"},for:{type:"string",value:"span",description:"how long: 12h, 30d, 2w"},until:{type:"string",value:"date",description:"when it ends (2026-12-31, or a time)"}},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the customer's id (the user's id)"),s=rn(e,1,"the entitlement"),a=e.flags.reason?.trim();if(!a)throw g("--reason says why it is given: the grant's record");if(a.length>500)throw g("--reason is 500 characters at most");let r=M$(e);await _(e,t,`give ${n} ${s} ${r?`until ${D(r)}`:"for life"}`);let i={entitlement:s,reason:a,...r?{expires_at:r}:{}},o=await m(e,t,"POST",`/purchases/customers/${S(n)}/grants`,i,x());e.out.result(o,()=>{e.out.line(`Gave ${o.customer_id} ${o.entitlement} ${o.expires_at?`until ${D(o.expires_at)}`:"for life"} (grant ${o.id}).`),e.out.line(`End it: palbase purchases:customer:revoke ${o.customer_id} ${o.id}`)})}},D$={name:"purchases:customer:revoke",summary:"end a granted entitlement now; what the customer bought is not touched",usage:"<customer id> <grant id>",details:["The grant's id is the one purchases:customer:grant printed. The entitlement stays on while a purchase still gives it."],args:{min:2,max:2},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the customer's id (the user's id)"),s=rn(e,1,"the grant's id");await _(e,t,`end the grant ${s} of ${n}`),await m(e,t,"DELETE",`/purchases/customers/${S(n)}/grants/${S(s)}`,void 0,x()),e.out.result({revoked:s,customer_id:n},()=>e.out.line(`Ended the grant ${s} of ${n}.`))}},N$={name:"purchases:stripe",summary:"the environment's Stripe account: whether one is connected, what Stripe last said of its key",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/purchases/stripe");e.out.result(n,()=>{if(!n.connected){e.out.line("No Stripe account is connected."),e.out.line("Connect one with one click: palbase purchases:stripe:install (Palbase's Stripe App), or with a restricted key: palbase purchases:stripe:grant");return}let s=n.via==="app"?"through Palbase's Stripe App":`key ${n.key_hint??"?"}`;e.out.line(`Stripe account ${n.account_id??"?"} (${n.livemode?"live":"test"} mode), ${s}`),e.out.line(`status: ${n.status??"not checked"}${n.checked_at?` (checked ${D(n.checked_at)})`:""}`),n.connected_at&&e.out.line(`connected ${D(n.connected_at)}${n.connected_by?` by ${n.connected_by}`:""}`),n.action&&e.out.line(`To do: ${n.action}`)})}},q$={name:"purchases:stripe:grant",summary:"a link that connects a Stripe restricted key: the key goes from the browser straight to the environment, never through Studio or this computer",details:["The link works once, for 30 minutes; a project holds 10 open at most."],scope:"environment",async run(e){let t=await b(e);await _(e,t,"make a link that connects a Stripe account");let n=await m(e,t,"POST","/purchases/stripe/grants",void 0,x());nn(e,t,n.connect_url,"the connect link"),e.out.result({connect_url:n.connect_url,expires_at:n.expires_at},()=>{e.out.line(n.connect_url),e.out.note(`Open it in a browser and paste the Stripe restricted key; it works once, until ${D(n.expires_at)}.`)})}},G$={name:"purchases:stripe:install",summary:"a link that connects the environment's Stripe account with one click: Palbase's Stripe App, installed at Stripe \u2014 no key pasted",details:["The link opens Palbase's install page, naming the project; from there Stripe: pick the account, accept what the App may do, and Stripe sends you back to Palbase, which connects it.","It works once, for 30 minutes; --live installs the App in live mode (Stripe test mode otherwise).","A restricted key connects an account too: palbase purchases:stripe:grant."],flags:{live:{type:"boolean",description:"install the App in live mode (test mode otherwise)"}},scope:"environment",async run(e){let t=await b(e),n=e.flags.live===!0;await _(e,t,`make a link that installs Palbase's Stripe App in ${n?"live":"test"} mode`);let s=await m(e,t,"POST","/purchases/stripe/app/grants",{livemode:n},x()),a=mo(e.cloud),r;try{r=new URL(s.install_url)}catch{r=void 0}if(r?.origin!==a||r.pathname!=="/purchases/stripe/app/install"||!r.hash.startsWith("#grant="))throw new y("install_link_not_palbase",`the install link names ${r?r.origin+r.pathname:s.install_url}, not Palbase's install page at ${a}: it is not printed`);e.out.result({install_url:s.install_url,expires_at:s.expires_at},()=>{e.out.line(s.install_url),e.out.note(`Open it in a browser, pick the Stripe account and accept; it works once, until ${D(s.expires_at)}.`)})}};function K$(e){let t=n=>/^[\w./@:-]+$/.test(n)?n:JSON.stringify(n);return`${e.global.project!==void 0?` -P ${t(e.global.project)}`:""}${e.global.env!==void 0?` -E ${t(e.global.env)}`:""}`}async function zi(e,t){let n=(await qe(e,t.project_id)).filter(o=>o.deleted_at===null),s=n.filter(o=>o.platform==="ios"),a=e.flags.app;if(a!==void 0){let o=Ye(n,a);if(o.platform!=="ios")throw g(`${o.slug} is a ${o.platform} app: a StoreKit configuration is an iOS app's`,`the project's iOS apps: ${s.map(l=>l.slug).join(", ")||"none"}`);return o}let r=(await ka(e.io.cwd))?.apps.filter(o=>o.platform==="ios").map(o=>o.id)??[],i=s.filter(o=>r.includes(o.id));if(i.length===1)return i[0];if(s.length===1)return s[0];throw s.length===0?new y("no_ios_app",`${t.project_name} has no iOS app`,{hint:"register it: palbase apps:create ios <name> --team-id <team id>"}):g("name the iOS app the file is for: --app <app>",`the project's iOS apps: ${s.map(o=>o.slug).join(", ")}`)}function U$(e,t){let n=k$(e.io.cwd,Va(e.io.cwd,t)).split(S$).join("/");if(!n.startsWith(`${G}/`)||n.split("/").includes(".."))throw g(`--out ${t}: the file goes under ${G}/, as the CLI writes nothing else in the checkout`,`palbase purchases:storekit --out ${G}/<name>.storekit`);if(!n.endsWith(".storekit"))throw g(`--out ${t}: Xcode takes a StoreKit configuration by its .storekit extension`);return n}function z$(e){let t=new Map;for(let n of e.products)for(let s of n.channels.ios?.plans??[])t.set(s.price.currency.toUpperCase(),(t.get(s.price.currency.toUpperCase())??0)+1);return[...t].sort(([n,s],[a,r])=>r-s||(n<a?-1:1))[0]?.[0]}var B$={name:"purchases:storekit",summary:"an Xcode StoreKit configuration of the environment's iOS products, under palbase/: a build Xcode runs buys them with no App Store",usage:"[--app <app>] [--out <palbase/\u2026.storekit>] [--storefront <code>]",details:["Each plan is the App Store product its store id names, with its text in every language the catalog has and its price; a subscription is in its group, at its tier's level, with its introductory offer and the catalog's offers on it as promotional offers (a grant has no store).","Prices are in the storefront's currency: the catalog's price, or the amount set by hand in that currency. The storefront is --storefront, else the one the file has, else the one the catalog's currency is the App Store's own price in (USD: USA, EUR: Germany, TRY: Turkey \u2026), else USA.","Made again, the file is made whole from the catalog \u2014 the same catalog, the same file \u2014 keeping the test settings Xcode's Editor menu wrote in it. Nothing outside palbase/ is written: Xcode is pointed at the file in the scheme."],flags:{app:{type:"string",value:"app",description:"the iOS app it is for (default: the one this checkout syncs, or the project's only one)"},out:{type:"string",value:"palbase/\u2026.storekit",description:"where under palbase/ (default: palbase/environments/<env>/<app>.storekit)"},storefront:{type:"string",value:"code",description:"the App Store storefront prices are in, by its three letters (USA, DEU, TUR, GBR, JPN \u2026)"}},scope:"environment",async run(e){let t=await b(e),n=e.flags.storefront?.trim().toUpperCase();if(n!==void 0&&!jn.has(n))throw g(`--storefront ${e.flags.storefront}: not an App Store storefront`,"a storefront is three letters, its country's ISO 3166-1 code: USA, DEU, TUR, GBR, JPN \u2026");let s=await zi(e,t);if(!/^[A-Za-z][A-Za-z0-9-]{0,38}$/.test(t.env_slug))throw new y("unexpected_answer",`the environment's slug ${JSON.stringify(t.env_slug)} is not one`);if(!/^[a-z][a-z0-9-]{0,38}[a-z0-9]$/.test(s.slug))throw new y("unexpected_answer",`the project answered app ${JSON.stringify(s.slug)}, which is not one`);let a=e.flags.out!==void 0?U$(e,e.flags.out):`${G}/environments/${t.env_slug}/${s.slug}.storekit`,r=await m(e,t,"GET","/purchases/offerings?channel=ios");if(!r.products.some(P=>(P.channels.ios?.plans.length??0)>0))throw new y("nothing_on_ios",`${t.env_name}'s catalog sells nothing on iOS yet: there is no product for Xcode to sell`,{hint:"give a product channels.ios in the catalog (palbase purchases:catalog:get --out catalog.json, then purchases:catalog:put)"});let i=await pe(e.io.cwd,a),o,l;try{o=await Ui(i,"utf8");let P=JSON.parse(o);typeof P.settings=="object"&&P.settings!==null&&!Array.isArray(P.settings)&&(l=P.settings)}catch{}let d=typeof l?._storefront=="string"&&jn.has(l._storefront)?l._storefront:void 0,p=z$(r);n??=d??(p!==void 0?cu(p):void 0)??"USA";let{catalog:c}=await m(e,t,"GET","/purchases/catalog"),h=gu(r,{seed:`${t.env_ref}/${s.id}`,storefront:n,settings:l,offers:c.offers??[]}),f=o!==h.text;f&&(await w$(_$(i),{recursive:!0}),await pe(e.io.cwd,a),await Vh(i,h.text));for(let P of h.notes)e.out.warn(P);let w=t.env_slug===Me,v=K$(e),j=[`In Xcode: Product \u203A Scheme \u203A Edit Scheme\u2026 \u203A Run \u203A Options \u203A StoreKit Configuration \u2014 choose ${a} (not listed yet? add it from that menu).`,w?`${t.project_name} \u203A ${t.env_name} is the project's production environment: it never takes a purchase Xcode signs. Make the file for a test environment (-E <env>) to see a purchase reach the server.`:`Then select the file in Xcode, choose Editor \u203A Save Public Certificate, and let ${t.env_name} take the purchases Xcode signs: palbase purchases:apple:xcode-certificate <the saved .cer file>${v}`,`Run the app and buy: Xcode sells the products itself, nothing goes to the App Store. After the catalog changes: palbase purchases:storekit${e.flags.app!==void 0?` --app ${s.slug}`:""}${e.flags.out!==void 0?` --out ${a}`:""}${v}`],k=h.counts,R=[k.subscriptions>0?`${k.subscriptions} subscription${k.subscriptions===1?"":"s"} in ${k.groups} group${k.groups===1?"":"s"}`:"",k.products>0?`${k.products} in-app purchase${k.products===1?"":"s"}`:"",k.non_renewing>0?`${k.non_renewing} non-renewing subscription${k.non_renewing===1?"":"s"}`:""].filter(P=>P!=="").join(", ");e.out.result({file:a,app:s.slug,environment:t.env_slug,revision:r.revision,storefront:h.storefront,currency:h.currency,locale:h.locale,...k,written:f,next:j},()=>{e.out.line(`${f?"Wrote":"Up to date:"} ${a} \u2014 ${R}, priced in ${h.currency} (storefront ${h.storefront}), catalog revision ${r.revision}.`),e.out.line(""),j.forEach((P,$)=>e.out.line(`${$+1}. ${P}`))})}},F$=/^SubscriptionKey_([A-Z0-9]{10})\.p8$/,W$=/^AuthKey_[A-Z0-9]{10}\.p8$/,J$=8192,V$=16384;function Zh(e,t){if(!t.connected)e.out.line("No In-App Purchase key is connected: the App Store's purchases are not taken."),e.out.line("Connect one: palbase purchases:apple:connect --key-file SubscriptionKey_<KEYID>.p8");else{e.out.line(`In-App Purchase key ${t.key_id??"?"} (issuer ${t.issuer_id??"?"}) for ${t.bundle_id??"?"}${t.app_apple_id?`, Apple ID ${t.app_apple_id}`:", its Apple ID not known yet"}`),e.out.line(`status: ${t.status??"not checked"}${t.checked_at?` (checked ${D(t.checked_at)})`:""}`);let s=t.notifications;s&&e.out.line(`notifications: ${s.url} \u2014 ${s.bound?`bound ${D(s.bound_at)}`:`not bound${s.error?`: ${s.error}`:""}`}; Apple's TEST notification ${s.verified_at?`arrived ${D(s.verified_at)}`:"not arrived yet"}`),t.connected_at&&e.out.line(`connected ${D(t.connected_at)}${t.connected_by?` by ${t.connected_by}`:""}`),t.action&&e.out.line(`To do: ${t.action}`)}let n=t.xcode_certificate;e.out.line(n?`Xcode's StoreKit Testing purchases: taken, signed with ${n.subject} until ${D(n.not_after)} (sha256 ${n.fingerprint.slice(0,16)}\u2026)`:"Xcode's StoreKit Testing purchases: not taken (palbase purchases:apple:xcode-certificate <file> takes them)")}var H$={name:"purchases:apple",summary:"the environment's App Store connection: the In-App Purchase key, where the App Store reports, whether Xcode's purchases are taken",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/purchases/apple");e.out.result(n,()=>Zh(e,n))}};function X$(e,t){let n=e.environments.find(s=>s.ref===t.env_ref)?.ios?.bundle;if(n===void 0)throw new y("app_not_in_environment",`${e.slug} is not set in ${t.env_name}: the key is connected for the app's bundle there`,{hint:`palbase apps:env:set ${e.slug} -E ${t.env_slug} --bundle <bundle id>`});return n}var Y$={name:"purchases:apple:connect",summary:"connect an In-App Purchase key for an iOS app: it goes from this computer straight to the environment, never through Studio",usage:"--key-file <SubscriptionKey_KEYID.p8> [--key-id <KEYID>] [--issuer-id <issuer id>] [--app <app>] | --print-link",details:["App Store Connect \u2192 Users and Access \u2192 Integrations \u2192 In-App Purchase makes the key (SubscriptionKey_<KEYID>.p8) and shows the Issuer ID \u2014 the team's, the same as its App Store Connect API keys': left out, the one stores:apple connected is taken.","The environment verifies the app's purchases and reads its subscriptions with it, and has the stores module point the App Store's notifications at the environment.","Studio is asked for a one-time grant alone; the key is posted with it to the environment's own address. --print-link prints a link that takes the key in a browser instead."],flags:{"key-file":{type:"string",value:"SubscriptionKey_KEYID.p8",description:"the .p8 file Apple gave when the key was made"},"key-id":{type:"string",value:"KEYID",description:"the key's id (default: from the file's name)"},"issuer-id":{type:"string",value:"issuer id",description:"the team's issuer id (default: the connected App Store Connect key's)"},app:{type:"string",value:"app",description:"the iOS app (default: the one this checkout syncs, or the project's only one)"},"print-link":{type:"boolean",description:"only print a link that takes the key in a browser (valid 30 minutes, once)"}},scope:"environment",async run(e){let t=await b(e);if(e.flags["print-link"]===!0){if(e.flags["key-file"]!==void 0)throw g("--print-link or --key-file, not both");await _(e,t,"make a link that connects an In-App Purchase key");let f=await m(e,t,"POST","/purchases/apple/grants",void 0,x());nn(e,t,f.connect_url,"the connect link"),e.out.result({connect_url:f.connect_url,expires_at:f.expires_at},()=>{e.out.line(f.connect_url),e.out.note(`Open it in a browser, pick the app and the .p8 file; it works once, until ${D(f.expires_at)}.`)});return}let n=e.flags["key-file"];if(n===void 0)throw g("--key-file names the In-App Purchase key's .p8 file","or --print-link for a link a browser opens");let s=v$(n),a=e.flags["key-id"];if(a===void 0){if(W$.test(s))throw g(`${s} is an App Store Connect API key (palbase stores:apple:connect takes it): this is the In-App Purchase key, SubscriptionKey_<KEYID>.p8`,"App Store Connect \u2192 Users and Access \u2192 Integrations \u2192 In-App Purchase makes one");if(a=F$.exec(s)?.[1],a===void 0)throw g(`--key-id is needed: ${s} is not named SubscriptionKey_<KEYID>.p8`)}let r;try{let f=Va(e.io.cwd,n);if((await Jh(f)).size>J$)throw new y("not_a_key",`${n} is larger than a .p8 key`);r=await Ui(f,"utf8")}catch(f){throw f instanceof y?f:new y("unreadable_file",`${n} could not be read: ${f instanceof Error?f.message:String(f)}`)}if(!r.includes("-----BEGIN PRIVATE KEY-----"))throw new y("not_a_key",`${n} is not a .p8 key (no "BEGIN PRIVATE KEY")`);let i=e.flags["issuer-id"]?.trim();if(i===void 0||i===""){let f=await m(e,t,"GET","/stores/apple");if(i=f.connected?f.issuer_id:void 0,i===void 0)throw g("--issuer-id is the team's issuer id (App Store Connect \u2192 Users and Access \u2192 Integrations): no App Store Connect key is connected to read it from")}let o=await zi(e,t),l=X$(o,t);await _(e,t,`connect the In-App Purchase key ${a} for ${l}`);let d=await m(e,t,"POST","/purchases/apple/grants",void 0,x()),p=nn(e,t,d.connect_url,"the connect link"),c={grant:d.grant,issuer_id:i,key_id:a,private_key:r,bundle_id:l},h=await qa(e,`${p}/purchases/apple/connect`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify(c)},"connecting the In-App Purchase key");e.out.result(h,()=>{e.out.line(`Connected key ${a} for ${l} to ${q(t)}.`),Zh(e,h),e.out.line("Check what else the App Store needs: palbase purchases:doctor")})}},Z$={name:"purchases:apple:disconnect",summary:"forget the environment's In-App Purchase key: the App Store's purchases and notifications are refused from then on (what was bought stays)",scope:"environment",async run(e){let t=await b(e);await _(e,t,"disconnect the In-App Purchase key"),await m(e,t,"DELETE","/purchases/apple",void 0,x()),e.out.result({disconnected:!0},()=>e.out.line(`Disconnected the In-App Purchase key from ${q(t)}.`))}};function Qh(e,t){if(t.service_account===void 0){e.out.line("The project has no Google Play connection: Palbase reads the app's purchases as the account your Play Console invites."),e.out.line("Connect it first: palbase stores:google:connect");return}if(e.out.line(`Play connection: ${t.service_account} (${t.play_status??"not checked"})`),!t.connected){e.out.line("Google Play purchases are not connected: palbase purchases:google:connect --app <android app>");return}e.out.line(`app: ${t.package_name??"?"}`),e.out.line(`topic: ${t.topic??"?"} (Play Console \u203A Monetize with Play \u203A Monetization setup \u203A Real-time developer notifications)`),e.out.line(`notifications: ${t.notification_url??"?"} \u2014 Play's test notification ${t.verified_at?`arrived ${D(t.verified_at)}`:"not arrived yet"}`),t.connected_at&&e.out.line(`connected ${D(t.connected_at)}${t.connected_by?` by ${t.connected_by}`:""}`),t.action&&e.out.line(`To do: ${t.action}`)}var Q$={name:"purchases:google",summary:"the environment's Google Play purchases: the app they are made in, the topic its Play Console names, whether Play's test notification arrived",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/purchases/google");e.out.result(n,()=>Qh(e,n))}};async function eP(e,t){let n=(await qe(e,t.project_id)).filter(o=>o.deleted_at===null),s=n.filter(o=>o.platform==="android"),a=e.flags.app;if(a!==void 0){let o=Ye(n,a);if(o.platform!=="android")throw g(`${o.slug} is a ${o.platform} app: Google Play purchases are an Android app's`,`the project's Android apps: ${s.map(l=>l.slug).join(", ")||"none"}`);return o}let r=(await ka(e.io.cwd))?.apps.filter(o=>o.platform==="android").map(o=>o.id)??[],i=s.filter(o=>r.includes(o.id));if(i.length===1)return i[0];if(s.length===1)return s[0];throw s.length===0?new y("no_android_app",`${t.project_name} has no Android app`,{hint:"register it: palbase apps:create android <name>"}):g("name the Android app: --app <app>",`the project's Android apps: ${s.map(o=>o.slug).join(", ")}`)}var tP={name:"purchases:google:connect",summary:"sell on Google Play in one of the project's Android apps: Palbase makes the app's notifications topic and has it pushed to this environment",usage:"[--app <app>]",details:["The project's Play connection (palbase stores:google:connect) is the account Palbase reads and acknowledges the app's purchases as: its Play Console invites it with View financial data and Manage orders and subscriptions.","Then, in Play Console: Monetize with Play \u203A Monetization setup \u203A Real-time developer notifications, the topic this prints, Save, and Send test notification \u2014 palbase purchases:google says when it arrived."],flags:{app:{type:"string",value:"app",description:"the Android app (default: the one this checkout syncs, or the project's only one)"}},scope:"environment",async run(e){let t=await b(e),n=await eP(e,t);await _(e,t,`connect Google Play purchases for ${n.slug}`);let s={app_id:n.id},a=await m(e,t,"POST","/purchases/google",s,x());e.out.result(a,()=>{e.out.line(`Google Play purchases connected for ${a.package_name??n.slug} in ${q(t)}.`),Qh(e,a)})}},nP={name:"purchases:google:disconnect",summary:"stop taking the environment's Google Play purchases: Play's notifications stop reaching it, and the app's reports are refused (what was bought stays)",scope:"environment",async run(e){let t=await b(e);await _(e,t,"disconnect Google Play purchases"),await m(e,t,"DELETE","/purchases/google",void 0,x()),e.out.result({disconnected:!0},()=>e.out.line(`Disconnected Google Play purchases from ${q(t)}.`))}};async function sP(e,t){let n=(await qe(e,t.project_id)).find(s=>s.deleted_at===null&&s.environments.some(a=>a.ref===t.env_ref));return n===void 0?t.env_slug===Me:(await e.studio.get(`/environments/${t.env_ref}/apps/${n.id}/config`)).environment.production}async function aP(e,t){let n;try{let r=Va(t,e);if((await Jh(r)).size>V$)throw new y("not_a_certificate",`${e} is larger than Xcode's certificate`);n=await Ui(r)}catch(r){throw r instanceof y?r:new y("unreadable_file",`${e} could not be read: ${r instanceof Error?r.message:String(r)}`)}let s;try{s=new b$(n)}catch{throw new y("not_a_certificate",`${e} is not a certificate`,{hint:"the one Xcode saves: select the .storekit file, then Editor \u203A Save Public Certificate"})}let a=n.toString("latin1");return{certificate:a.includes("-----BEGIN CERTIFICATE-----")?a.trim():n.toString("base64"),x509:s}}var rP={name:"purchases:apple:xcode-certificate",summary:"take the purchases Xcode's StoreKit Testing signs with this certificate, as test purchases \u2014 never on production; --remove stops",usage:"<certificate.cer> | --remove",details:["Xcode saves the certificate: select the .storekit file (palbase purchases:storekit makes it), then Editor \u203A Save Public Certificate.","From then on what the app reports signed with it \u2014 Xcode's own purchases, on a developer's Mac \u2014 is a test purchase of the environment, with or without an In-App Purchase key; nothing of it is asked of Apple. Set again, it replaces the one before.","The production environment takes only what the App Store signed: it is refused there."],args:{min:0,max:1},flags:{remove:{type:"boolean",description:"stop taking Xcode's purchases (those taken stay)"}},scope:"environment",async run(e){let t=await b(e),n=e.flags.remove===!0,s=e.args[0];if(n===(s!==void 0))throw g("name the certificate file, or --remove","palbase purchases:apple:xcode-certificate StoreKitTestCertificate.cer");if(n){await _(e,t,"stop taking Xcode's StoreKit Testing purchases"),await m(e,t,"DELETE","/purchases/apple/xcode-certificate",void 0,x()),e.out.result({removed:!0},()=>e.out.line(`${q(t)} no longer takes Xcode's purchases; those it took stay.`));return}let a=await aP(s,e.io.cwd);if(await sP(e,t))throw new y("production_environment",`${t.project_name} \u203A ${t.env_name} is the project's production environment: it takes only purchases the App Store signed, never Xcode's`,{hint:"set the certificate on a test environment: palbase purchases:apple:xcode-certificate <file> -E <env>"});await _(e,t,`take the purchases Xcode signs with ${a.x509.subject.replaceAll(`
|
|
675
|
+
`),e.out.result({file:s,revision:n.revision},()=>{e.out.line(`Wrote revision ${n.revision} of the catalog to ${s}.`),e.out.line(`Save an edit of it: palbase purchases:catalog:put --file ${s} --revision ${n.revision}`)});return}e.out.result(n,()=>P$(e,n))}},R$={name:"purchases:catalog:put",summary:"save a new revision of the whole catalog, on the revision it was edited from; nothing is written to a store",usage:"--file <catalog.json> --revision <n>",details:["The module checks it first: a catalog that breaks a rule is refused with every problem, and nothing is saved.","Of two people editing at once, the second is told \u2014 the catalog moved past the revision they edited \u2014 never overwritten.","The stores are brought to it by a plan: palbase purchases:catalog:plan."],flags:{file:{type:"string",value:"catalog.json",description:"the catalog, as catalog:get --out wrote it ('-' or none: stdin)"},revision:{type:"string",value:"n",description:"the revision it was edited from (0 for the first), as catalog:get printed it"}},scope:"environment",async run(e){let t=await b(e),n=le(e.flags.revision,"revision");if(n===void 0)throw g("--revision is the revision the catalog was edited from (0 for the first): of two edits at once, the second is told, not saved over the first");let s=await Hh(e);await _(e,t,`save the catalog (${s.products?.length??0} products) over revision ${n}`);let a;try{a=await m(e,t,"PUT","/purchases/catalog",s,{"if-match":$$(n),...x()})}catch(r){if(r instanceof I&&r.status===412)throw new y("catalog_changed",`the catalog is no longer at revision ${n}: someone saved a newer one since it was read \u2014 nothing was saved`,{hint:"read it again (palbase purchases:catalog:get --out <file>), make the edit on it, and save it with that revision"});Yh(e,r,"the catalog was not saved")}e.out.result(a,()=>{e.out.line(`Saved revision ${a.revision} of the catalog.`),e.out.line("Bring the stores to it: palbase purchases:catalog:plan")})}},T$={name:"purchases:catalog:validate",summary:"check a catalog without saving it: every problem, with its path and the store whose rule it breaks",usage:"--file <catalog.json>",flags:{file:{type:"string",value:"catalog.json",description:"the catalog ('-' or none: stdin)"}},scope:"environment",async run(e){let t=await b(e),n=await Hh(e),s;try{s=await m(e,t,"POST","/purchases/catalog/validate",n,x())}catch(a){Yh(e,a,"the catalog cannot be read")}if(e.out.result(s,()=>{s.problems.length>0&&Xh(e,s.problems),e.out.line(s.valid?`The catalog can be saved${s.problems.length>0?`, with ${s.problems.length} warning(s)`:""}.`:"The catalog cannot be saved as it is.")}),!s.valid)throw new y("invalid_catalog",`the catalog breaks ${s.problems.filter(a=>!a.warning).length} rule(s)`)}};function Ki(e,t,n){e.out.line(`${t}${n.store_env?` (${n.store_env})`:""}: ${n.steps.length===0?"nothing to write":`${n.steps.length} step(s)`}`);for(let s of n.steps)e.out.line(` ${s.op} ${s.product}${s.plan?`/${s.plan}`:""}${s.store_id?` (${s.store_id})`:""}${s.irreversible?" \u2014 permanent":""}${s.note?`: ${s.note}`:""}`);n.note&&e.out.line(` ${n.note}`)}function E$(e,t){if(e.out.line(`Plan ${t.id} for revision ${t.revision}: ${t.status}`),t.stripe?Ki(e,Ja.stripe,t.stripe):e.out.line(`${Ja.stripe}: no account connected (palbase purchases:stripe:grant connects one)`),Ki(e,Ja.apple,t.apple),Ki(e,Ja.google,t.google),t.problems.length>0){e.out.line("Warnings of the revision:");for(let r of t.problems)e.out.line(` ${r.path}: ${r.message}${r.channel?` (${r.channel})`:""}`)}t.status==="failed"&&t.error&&e.out.line(`Stripe answered: ${t.error}`);let n=t.apple_result;if(n){e.out.line(`App Store Connect (${n.bundle_id}): ${n.ok?"written":`not all written${n.error?`: ${n.error}`:""}`}`);for(let r of n.steps){let i=r.price?`, ${r.price.customer_price} ${r.price.currency}`:"",o=r.next_price?`, ${r.next_price.customer_price} ${r.next_price.currency} from ${r.next_price.from}`:"";e.out.line(` ${r.op} ${r.store_id}${r.state?`: ${r.state}`:""}${i}${o}${r.error?` \u2014 ${r.error}`:""}`)}}let s=t.google_result;if(s){e.out.line(`Google Play (${s.package_name}): ${s.pending?"waiting for the stores module":s.ok?"written":`not all written${s.error?`: ${s.error}`:""}`}`);for(let r of s.steps)e.out.line(` ${r.op} ${r.store_id}${r.state?`: ${r.state}`:""}${r.error?` \u2014 ${r.error}`:""}`)}let a=t.stripe?t.stripe.hash:t.apple.hash||t.google.hash;t.status==="planned"&&a&&(e.out.line(` hash: ${a}`),e.out.line(t.stripe?`The Stripe part is written only when confirmed${t.apple.steps.length>0?" (the App Store part goes with it, to the stores module)":""}:`:t.apple.hash?"The App Store part is written only when confirmed (the stores module writes it to App Store Connect):":"The Google Play part is written only when confirmed (the stores module writes it to Google Play):"),e.out.line(` palbase purchases:catalog:confirm ${t.id} --hash ${a}`))}var C$={name:"purchases:catalog:plan",summary:"plan bringing the stores to the catalog's newest revision, store by store, before anything is written",usage:"[<plan id>]",details:["A plan is confirmed with its Stripe part's hash (purchases:catalog:confirm) \u2014 with no Stripe account connected, its App Store part's; the App Store and Google Play parts are written by the stores module once their accounts are connected.","With a plan's id it makes none: it shows that plan, and once confirmed what each store answered \u2014 the state of every product written, and a price that changes on a later day."],args:{min:0,max:1},scope:"environment",async run(e){let t=await b(e),n=e.args[0],s=n===void 0?await m(e,t,"POST","/purchases/catalog/plans",void 0,x()):await m(e,t,"GET",`/purchases/catalog/plans/${S(n)}`);e.out.result(s,()=>E$(e,s))}},j$={name:"purchases:catalog:confirm",summary:"write a plan \u2014 its Stripe part now, its App Store part through the stores module: it names the hash of the plan that was shown, and is asked about first",usage:"<plan id> --hash <hash>",details:["What it writes is shown, then asked about \u2014 in every environment: a price written to Stripe is never changed or removed there, a product made in App Store Connect never goes away. -y confirms without asking."],args:{min:1,max:1},flags:{hash:{type:"string",value:"hash",description:"the hash the plan printed (its Stripe part's; with no Stripe account, its App Store part's)"}},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the plan's id"),s=e.flags.hash;if(s===void 0||s.trim()==="")throw g("--hash is the hash purchases:catalog:plan printed: a confirmation names the plan it saw");let a=await m(e,t,"GET",`/purchases/catalog/plans/${S(n)}`);if(a.status!=="planned")throw new y("plan_not_open",`the plan ${n} is ${a.status}: only a planned one is confirmed`,{hint:"make a new one: palbase purchases:catalog:plan"});let r=a.apple.steps.length;if(a.stripe)await _(e,t,`write ${a.stripe.steps.length} step(s) of revision ${a.revision} to Stripe${r>0?`, and ${r} to App Store Connect`:""}`,"It writes the project's Stripe account, where a price is never changed or removed");else{if(!a.apple.hash||r===0)throw new y("no_stripe_part",`the plan ${n} has no Stripe part (no Stripe account was connected when it was made) and no App Store part to confirm`);await _(e,t,`write ${r} step(s) of revision ${a.revision} to App Store Connect`,"It makes the products in App Store Connect, where a product id never goes away")}let i=await m(e,t,"POST",`/purchases/catalog/plans/${S(n)}/confirm`,{hash:s},x());if(e.out.result(i,()=>{e.out.line(`Plan ${i.id}: ${i.status}${i.applied_at?` (${D(i.applied_at)})`:""}.`),i.status==="failed"&&i.error&&e.out.line(`Stripe answered: ${i.error}`),r>0&&!i.apple_result&&e.out.line(`The stores module is writing the App Store part to App Store Connect: palbase purchases:catalog:plan ${i.id} shows what it wrote.`)}),i.status==="failed")throw new y("plan_failed",`the plan ${i.id} failed: a new plan picks up where it stopped`)}},O$={name:"purchases:catalog:decline",summary:"decline a plan: its Stripe part is never written",usage:"<plan id>",args:{min:1,max:1},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the plan's id");await _(e,t,`decline the plan ${n}`);let s=await m(e,t,"POST",`/purchases/catalog/plans/${S(n)}/decline`,void 0,x());e.out.result(s,()=>e.out.line(`Declined the plan ${s.id}.`))}};function Wh(e){return e===null?"for life":D(e)}function x$(e,t){e.out.line(`Customer ${t.customer_id}${t.guest?" (guest)":""}: revision ${t.revision}, as of ${D(t.requested_at)}`);let n=Object.entries(t.entitlements).sort(([s],[a])=>s<a?-1:s>a?1:0);e.out.line(n.length===0?"No entitlement.":"Entitlements:"),e.out.table(n,[{header:"entitlement",value:([s])=>s},{header:"active",value:([,s])=>s.active?"yes":"no"},{header:"until",value:([,s])=>Wh(s.expires_at)},{header:"store",value:([,s])=>`${s.store}${s.is_sandbox?" (sandbox)":""}`},{header:"renews",value:([,s])=>s.will_renew?"yes":"no"},{header:"product",value:([,s])=>s.product??"\u2014"}]),t.subscriptions.length>0&&(e.out.line("Subscriptions:"),e.out.table(t.subscriptions,[{header:"product",value:s=>`${s.product??"\u2014"}${s.plan?`/${s.plan}`:""}`},{header:"store",value:s=>`${s.store}${s.is_sandbox?" (sandbox)":""}`},{header:"state",value:s=>s.state},{header:"until",value:s=>Wh(s.expires_at)},{header:"renews",value:s=>s.will_renew?"yes":"no"}])),t.purchases.length>0&&(e.out.line("Purchases:"),e.out.table(t.purchases,[{header:"product",value:s=>s.product??"\u2014"},{header:"store",value:s=>`${s.store}${s.is_sandbox?" (sandbox)":""}`},{header:"state",value:s=>s.state},{header:"bought",value:s=>D(s.purchased_at)}])),t.credits.length>0&&(e.out.line("Credits:"),e.out.table(t.credits,[{header:"credit",value:s=>s.currency},{header:"balance",value:s=>String(s.balance)},{header:"available",value:s=>String(s.available)},{header:"held",value:s=>String(s.reserved)},{header:"owed",value:s=>String(s.debt)},{header:"next expiry",value:s=>D(s.next_expiry)}])),t.limits.length>0&&(e.out.line("Limits:"),e.out.table(t.limits,[{header:"limit",value:s=>s.limit},{header:"used",value:s=>`${s.used} of ${s.max}`},{header:"held",value:s=>String(s.reserved)},{header:"left",value:s=>String(s.remaining)},{header:"resets",value:s=>D(s.resets_at)}]))}var I$={name:"purchases:customer:get",summary:"what a customer has, whatever store they bought in: entitlements, subscriptions, purchases, credits and limits",usage:"<customer id>",details:["The customer is the user's id; a guest merged into an account answers as the account."],args:{min:1,max:1},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the customer's id (the user's id)"),s=await m(e,t,"GET",`/purchases/customers/${S(n)}`);e.out.result(s,()=>x$(e,s))}};function M$(e){let t=e.flags.until,n=e.flags.for;if(t!==void 0&&n!==void 0)throw g("--until or --for, not both");if(t!==void 0){let s=Date.parse(t);if(!Number.isFinite(s))throw g(`--until ${t}: a date or time (2026-12-31, 2026-12-31T18:00:00Z)`);if(s<=e.io.now())throw g(`--until ${t} is not in the future`);return new Date(s).toISOString()}if(n!==void 0){let s=/^([1-9][0-9]{0,4})([hdw])$/.exec(n.trim());if(!s)throw g(`--for ${n}: a number of hours, days or weeks (12h, 30d, 2w)`);let a=Number(s[1])*{h:1,d:24,w:168}[s[2]];return new Date(e.io.now()+a*36e5).toISOString()}}var L$={name:"purchases:customer:grant",summary:"give a customer one of the catalog's entitlements, for a while or for life, saying why \u2014 a promotion, a support case, a tester",usage:"<customer id> <entitlement> --reason <why> [--for 30d | --until <date>]",details:["The reason is the record. Without --for or --until the grant is for life. It prints the grant's id: purchases:customer:revoke ends it.","A customer not seen before is made; nothing is charged, in any store."],args:{min:2,max:2},flags:{reason:{type:"string",value:"why",description:"why it is given (500 characters at most): the record"},for:{type:"string",value:"span",description:"how long: 12h, 30d, 2w"},until:{type:"string",value:"date",description:"when it ends (2026-12-31, or a time)"}},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the customer's id (the user's id)"),s=rn(e,1,"the entitlement"),a=e.flags.reason?.trim();if(!a)throw g("--reason says why it is given: the grant's record");if(a.length>500)throw g("--reason is 500 characters at most");let r=M$(e);await _(e,t,`give ${n} ${s} ${r?`until ${D(r)}`:"for life"}`);let i={entitlement:s,reason:a,...r?{expires_at:r}:{}},o=await m(e,t,"POST",`/purchases/customers/${S(n)}/grants`,i,x());e.out.result(o,()=>{e.out.line(`Gave ${o.customer_id} ${o.entitlement} ${o.expires_at?`until ${D(o.expires_at)}`:"for life"} (grant ${o.id}).`),e.out.line(`End it: palbase purchases:customer:revoke ${o.customer_id} ${o.id}`)})}},D$={name:"purchases:customer:revoke",summary:"end a granted entitlement now; what the customer bought is not touched",usage:"<customer id> <grant id>",details:["The grant's id is the one purchases:customer:grant printed. The entitlement stays on while a purchase still gives it."],args:{min:2,max:2},scope:"environment",async run(e){let t=await b(e),n=rn(e,0,"the customer's id (the user's id)"),s=rn(e,1,"the grant's id");await _(e,t,`end the grant ${s} of ${n}`),await m(e,t,"DELETE",`/purchases/customers/${S(n)}/grants/${S(s)}`,void 0,x()),e.out.result({revoked:s,customer_id:n},()=>e.out.line(`Ended the grant ${s} of ${n}.`))}},N$={name:"purchases:stripe",summary:"the environment's Stripe account: whether one is connected, what Stripe last said of its key",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/purchases/stripe");e.out.result(n,()=>{if(!n.connected){e.out.line("No Stripe account is connected."),e.out.line("Connect one with one click: palbase purchases:stripe:install (Palbase's Stripe App), or with a restricted key: palbase purchases:stripe:grant");return}let s=n.via==="app"?"through Palbase's Stripe App":`key ${n.key_hint??"?"}`;e.out.line(`Stripe account ${n.account_id??"?"} (${n.livemode?"live":"test"} mode), ${s}`),e.out.line(`status: ${n.status??"not checked"}${n.checked_at?` (checked ${D(n.checked_at)})`:""}`),n.connected_at&&e.out.line(`connected ${D(n.connected_at)}${n.connected_by?` by ${n.connected_by}`:""}`),n.action&&e.out.line(`To do: ${n.action}`)})}},q$={name:"purchases:stripe:grant",summary:"a link that connects a Stripe restricted key: the key goes from the browser straight to the environment, never through Studio or this computer",details:["The link works once, for 30 minutes; a project holds 10 open at most."],scope:"environment",async run(e){let t=await b(e);await _(e,t,"make a link that connects a Stripe account");let n=await m(e,t,"POST","/purchases/stripe/grants",void 0,x());nn(e,t,n.connect_url,"the connect link"),e.out.result({connect_url:n.connect_url,expires_at:n.expires_at},()=>{e.out.line(n.connect_url),e.out.note(`Open it in a browser and paste the Stripe restricted key; it works once, until ${D(n.expires_at)}.`)})}},G$={name:"purchases:stripe:install",summary:"a link that connects the environment's Stripe account with one click: Palbase's Stripe App, installed at Stripe \u2014 no key pasted",details:["The link opens Palbase's install page, naming the project; from there Stripe: pick the account, accept what the App may do, and Stripe sends you back to Palbase, which connects it.","It works once, for 30 minutes; --live installs the App in live mode (Stripe test mode otherwise).","A restricted key connects an account too: palbase purchases:stripe:grant."],flags:{live:{type:"boolean",description:"install the App in live mode (test mode otherwise)"}},scope:"environment",async run(e){let t=await b(e),n=e.flags.live===!0;await _(e,t,`make a link that installs Palbase's Stripe App in ${n?"live":"test"} mode`);let s=await m(e,t,"POST","/purchases/stripe/app/grants",{livemode:n},x()),a=mo(e.cloud),r;try{r=new URL(s.install_url)}catch{r=void 0}if(r?.origin!==a||r.pathname!=="/purchases/stripe/app/install"||!r.hash.startsWith("#grant="))throw new y("install_link_not_palbase",`the install link names ${r?r.origin+r.pathname:s.install_url}, not Palbase's install page at ${a}: it is not printed`);e.out.result({install_url:s.install_url,expires_at:s.expires_at},()=>{e.out.line(s.install_url),e.out.note(`Open it in a browser, pick the Stripe account and accept; it works once, until ${D(s.expires_at)}.`)})}};function K$(e){let t=n=>/^[\w./@:-]+$/.test(n)?n:JSON.stringify(n);return`${e.global.project!==void 0?` -P ${t(e.global.project)}`:""}${e.global.env!==void 0?` -E ${t(e.global.env)}`:""}`}async function zi(e,t){let n=(await qe(e,t.project_id)).filter(o=>o.deleted_at===null),s=n.filter(o=>o.platform==="ios"),a=e.flags.app;if(a!==void 0){let o=Ye(n,a);if(o.platform!=="ios")throw g(`${o.slug} is a ${o.platform} app: a StoreKit configuration is an iOS app's`,`the project's iOS apps: ${s.map(l=>l.slug).join(", ")||"none"}`);return o}let r=(await ka(e.io.cwd))?.apps.filter(o=>o.platform==="ios").map(o=>o.id)??[],i=s.filter(o=>r.includes(o.id));if(i.length===1)return i[0];if(s.length===1)return s[0];throw s.length===0?new y("no_ios_app",`${t.project_name} has no iOS app`,{hint:"register it: palbase apps:create ios <name> --team-id <team id>"}):g("name the iOS app the file is for: --app <app>",`the project's iOS apps: ${s.map(o=>o.slug).join(", ")}`)}function U$(e,t){let n=k$(e.io.cwd,Va(e.io.cwd,t)).split(S$).join("/");if(!n.startsWith(`${G}/`)||n.split("/").includes(".."))throw g(`--out ${t}: the file goes under ${G}/, as the CLI writes nothing else in the checkout`,`palbase purchases:storekit --out ${G}/<name>.storekit`);if(!n.endsWith(".storekit"))throw g(`--out ${t}: Xcode takes a StoreKit configuration by its .storekit extension`);return n}function z$(e){let t=new Map;for(let n of e.products)for(let s of n.channels.ios?.plans??[])t.set(s.price.currency.toUpperCase(),(t.get(s.price.currency.toUpperCase())??0)+1);return[...t].sort(([n,s],[a,r])=>r-s||(n<a?-1:1))[0]?.[0]}var B$={name:"purchases:storekit",summary:"an Xcode StoreKit configuration of the environment's iOS products, under palbase/: a build Xcode runs buys them with no App Store",usage:"[--app <app>] [--out <palbase/\u2026.storekit>] [--storefront <code>]",details:["Each plan is the App Store product its store id names, with its text in every language the catalog has and its price; a subscription is in its group, at its tier's level, with its introductory offer and the catalog's offers on it as promotional offers (a grant has no store).","Prices are in the storefront's currency: the catalog's price, or the amount set by hand in that currency. The storefront is --storefront, else the one the file has, else the one the catalog's currency is the App Store's own price in (USD: USA, EUR: Germany, TRY: Turkey \u2026), else USA.","Made again, the file is made whole from the catalog \u2014 the same catalog, the same file \u2014 keeping the test settings Xcode's Editor menu wrote in it. Nothing outside palbase/ is written: Xcode is pointed at the file in the scheme."],flags:{app:{type:"string",value:"app",description:"the iOS app it is for (default: the one this checkout syncs, or the project's only one)"},out:{type:"string",value:"palbase/\u2026.storekit",description:"where under palbase/ (default: palbase/environments/<env>/<app>.storekit)"},storefront:{type:"string",value:"code",description:"the App Store storefront prices are in, by its three letters (USA, DEU, TUR, GBR, JPN \u2026)"}},scope:"environment",async run(e){let t=await b(e),n=e.flags.storefront?.trim().toUpperCase();if(n!==void 0&&!jn.has(n))throw g(`--storefront ${e.flags.storefront}: not an App Store storefront`,"a storefront is three letters, its country's ISO 3166-1 code: USA, DEU, TUR, GBR, JPN \u2026");let s=await zi(e,t);if(!/^[A-Za-z][A-Za-z0-9-]{0,38}$/.test(t.env_slug))throw new y("unexpected_answer",`the environment's slug ${JSON.stringify(t.env_slug)} is not one`);if(!/^[a-z][a-z0-9-]{0,38}[a-z0-9]$/.test(s.slug))throw new y("unexpected_answer",`the project answered app ${JSON.stringify(s.slug)}, which is not one`);let a=e.flags.out!==void 0?U$(e,e.flags.out):`${G}/environments/${t.env_slug}/${s.slug}.storekit`,r=await m(e,t,"GET","/purchases/offerings?channel=ios");if(!r.products.some(P=>(P.channels.ios?.plans.length??0)>0))throw new y("nothing_on_ios",`${t.env_name}'s catalog sells nothing on iOS yet: there is no product for Xcode to sell`,{hint:"give a product channels.ios in the catalog (palbase purchases:catalog:get --out catalog.json, then purchases:catalog:put)"});let i=await pe(e.io.cwd,a),o,l;try{o=await Ui(i,"utf8");let P=JSON.parse(o);typeof P.settings=="object"&&P.settings!==null&&!Array.isArray(P.settings)&&(l=P.settings)}catch{}let d=typeof l?._storefront=="string"&&jn.has(l._storefront)?l._storefront:void 0,p=z$(r);n??=d??(p!==void 0?cu(p):void 0)??"USA";let{catalog:c}=await m(e,t,"GET","/purchases/catalog"),h=gu(r,{seed:`${t.env_ref}/${s.id}`,storefront:n,settings:l,offers:c.offers??[]}),f=o!==h.text;f&&(await w$(_$(i),{recursive:!0}),await pe(e.io.cwd,a),await Vh(i,h.text));for(let P of h.notes)e.out.warn(P);let w=t.env_slug===Me,v=K$(e),j=[`In Xcode: Product \u203A Scheme \u203A Edit Scheme\u2026 \u203A Run \u203A Options \u203A StoreKit Configuration \u2014 choose ${a} (not listed yet? add it from that menu).`,w?`${t.project_name} \u203A ${t.env_name} is the project's production environment: it never takes a purchase Xcode signs. Make the file for a test environment (-E <env>) to see a purchase reach the server.`:`Then select the file in Xcode, choose Editor \u203A Save Public Certificate, and let ${t.env_name} take the purchases Xcode signs: palbase purchases:apple:xcode-certificate <the saved .cer file>${v}`,`Run the app and buy: Xcode sells the products itself, nothing goes to the App Store. After the catalog changes: palbase purchases:storekit${e.flags.app!==void 0?` --app ${s.slug}`:""}${e.flags.out!==void 0?` --out ${a}`:""}${v}`],k=h.counts,R=[k.subscriptions>0?`${k.subscriptions} subscription${k.subscriptions===1?"":"s"} in ${k.groups} group${k.groups===1?"":"s"}`:"",k.products>0?`${k.products} in-app purchase${k.products===1?"":"s"}`:"",k.non_renewing>0?`${k.non_renewing} non-renewing subscription${k.non_renewing===1?"":"s"}`:""].filter(P=>P!=="").join(", ");e.out.result({file:a,app:s.slug,environment:t.env_slug,revision:r.revision,storefront:h.storefront,currency:h.currency,locale:h.locale,...k,written:f,next:j},()=>{e.out.line(`${f?"Wrote":"Up to date:"} ${a} \u2014 ${R}, priced in ${h.currency} (storefront ${h.storefront}), catalog revision ${r.revision}.`),e.out.line(""),j.forEach((P,$)=>e.out.line(`${$+1}. ${P}`))})}},F$=/^SubscriptionKey_([A-Z0-9]{10})\.p8$/,W$=/^AuthKey_[A-Z0-9]{10}\.p8$/,J$=8192,V$=16384;function Zh(e,t){if(!t.connected)e.out.line("No In-App Purchase key is connected: the App Store's purchases are not taken."),e.out.line("Connect one: palbase purchases:apple:connect --key-file SubscriptionKey_<KEYID>.p8");else{e.out.line(`In-App Purchase key ${t.key_id??"?"} (issuer ${t.issuer_id??"?"}) for ${t.bundle_id??"?"}${t.app_apple_id?`, Apple ID ${t.app_apple_id}`:", its Apple ID not known yet"}`),e.out.line(`status: ${t.status??"not checked"}${t.checked_at?` (checked ${D(t.checked_at)})`:""}`);let s=t.notifications;s&&e.out.line(`notifications: ${s.url} \u2014 ${s.bound?`bound ${D(s.bound_at)}`:`not bound${s.error?`: ${s.error}`:""}`}; Apple's TEST notification ${s.verified_at?`arrived ${D(s.verified_at)}`:"not arrived yet"}`),t.connected_at&&e.out.line(`connected ${D(t.connected_at)}${t.connected_by?` by ${t.connected_by}`:""}`),t.action&&e.out.line(`To do: ${t.action}`)}let n=t.xcode_certificate;e.out.line(n?`Xcode's StoreKit Testing purchases: taken, signed with ${n.subject} until ${D(n.not_after)} (sha256 ${n.fingerprint.slice(0,16)}\u2026)`:"Xcode's StoreKit Testing purchases: not taken (palbase purchases:apple:xcode-certificate <file> takes them)")}var H$={name:"purchases:apple",summary:"the environment's App Store connection: the In-App Purchase key, where the App Store reports, whether Xcode's purchases are taken",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/purchases/apple");e.out.result(n,()=>Zh(e,n))}};function X$(e,t){let n=e.environments.find(s=>s.ref===t.env_ref)?.ios?.bundle;if(n===void 0)throw new y("app_not_in_environment",`${e.slug} is not set in ${t.env_name}: the key is connected for the app's bundle there`,{hint:`palbase apps:env:set ${e.slug} -E ${t.env_slug} --bundle <bundle id>`});return n}var Y$={name:"purchases:apple:connect",summary:"connect an In-App Purchase key for an iOS app: it goes from this computer straight to the environment, never through Studio",usage:"--key-file <SubscriptionKey_KEYID.p8> [--key-id <KEYID>] [--issuer-id <issuer id>] [--app <app>] | --print-link",details:["App Store Connect \u2192 Users and Access \u2192 Integrations \u2192 In-App Purchase makes the key (SubscriptionKey_<KEYID>.p8) and shows the Issuer ID \u2014 the team's, the same as its App Store Connect API keys': left out, the one stores:apple connected is taken.","The environment verifies the app's purchases and reads its subscriptions with it, and has the stores module point the App Store's notifications at the environment.","Studio is asked for a one-time grant alone; the key is posted with it to the environment's own address. --print-link prints a link that takes the key in a browser instead."],flags:{"key-file":{type:"string",value:"SubscriptionKey_KEYID.p8",description:"the .p8 file Apple gave when the key was made"},"key-id":{type:"string",value:"KEYID",description:"the key's id (default: from the file's name)"},"issuer-id":{type:"string",value:"issuer id",description:"the team's issuer id (default: the connected App Store Connect key's)"},app:{type:"string",value:"app",description:"the iOS app (default: the one this checkout syncs, or the project's only one)"},"print-link":{type:"boolean",description:"only print a link that takes the key in a browser (valid 30 minutes, once)"}},scope:"environment",async run(e){let t=await b(e);if(e.flags["print-link"]===!0){if(e.flags["key-file"]!==void 0)throw g("--print-link or --key-file, not both");await _(e,t,"make a link that connects an In-App Purchase key");let f=await m(e,t,"POST","/purchases/apple/grants",void 0,x());nn(e,t,f.connect_url,"the connect link"),e.out.result({connect_url:f.connect_url,expires_at:f.expires_at},()=>{e.out.line(f.connect_url),e.out.note(`Open it in a browser, pick the app and the .p8 file; it works once, until ${D(f.expires_at)}.`)});return}let n=e.flags["key-file"];if(n===void 0)throw g("--key-file names the In-App Purchase key's .p8 file","or --print-link for a link a browser opens");let s=v$(n),a=e.flags["key-id"];if(a===void 0){if(W$.test(s))throw g(`${s} is an App Store Connect API key (palbase stores:apple:connect takes it): this is the In-App Purchase key, SubscriptionKey_<KEYID>.p8`,"App Store Connect \u2192 Users and Access \u2192 Integrations \u2192 In-App Purchase makes one");if(a=F$.exec(s)?.[1],a===void 0)throw g(`--key-id is needed: ${s} is not named SubscriptionKey_<KEYID>.p8`)}let r;try{let f=Va(e.io.cwd,n);if((await Jh(f)).size>J$)throw new y("not_a_key",`${n} is larger than a .p8 key`);r=await Ui(f,"utf8")}catch(f){throw f instanceof y?f:new y("unreadable_file",`${n} could not be read: ${f instanceof Error?f.message:String(f)}`)}if(!r.includes("-----BEGIN PRIVATE KEY-----"))throw new y("not_a_key",`${n} is not a .p8 key (no "BEGIN PRIVATE KEY")`);let i=e.flags["issuer-id"]?.trim();if(i===void 0||i===""){let f=await m(e,t,"GET","/stores/apple");if(i=f.connected?f.issuer_id:void 0,i===void 0)throw g("--issuer-id is the team's issuer id (App Store Connect \u2192 Users and Access \u2192 Integrations): no App Store Connect key is connected to read it from")}let o=await zi(e,t),l=X$(o,t);await _(e,t,`connect the In-App Purchase key ${a} for ${l}`);let d=await m(e,t,"POST","/purchases/apple/grants",void 0,x()),p=nn(e,t,d.connect_url,"the connect link"),c={grant:d.grant,issuer_id:i,key_id:a,private_key:r,bundle_id:l},h=await qa(e,`${p}/purchases/apple/connect`,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify(c)},"connecting the In-App Purchase key");e.out.result(h,()=>{e.out.line(`Connected key ${a} for ${l} to ${q(t)}.`),Zh(e,h),e.out.line("Check what else the App Store needs: palbase purchases:doctor")})}},Z$={name:"purchases:apple:disconnect",summary:"forget the environment's In-App Purchase key: the App Store's purchases and notifications are refused from then on (what was bought stays)",scope:"environment",async run(e){let t=await b(e);await _(e,t,"disconnect the In-App Purchase key"),await m(e,t,"DELETE","/purchases/apple",void 0,x()),e.out.result({disconnected:!0},()=>e.out.line(`Disconnected the In-App Purchase key from ${q(t)}.`))}};function Qh(e,t){if(t.service_account===void 0){e.out.line("The project has no Google Play connection: Palbase reads the app's purchases as the account your Play Console invites."),e.out.line("Connect it first: palbase stores:google:connect");return}if(e.out.line(`Play connection: ${t.service_account} (${t.play_status??"not checked"})`),!t.connected){e.out.line("Google Play purchases are not connected: palbase purchases:google:connect --app <android app>");return}e.out.line(`app: ${t.package_name??"?"}`),e.out.line(`topic: ${t.topic??"?"} (Play Console \u203A Monetize with Play \u203A Monetization setup \u203A Real-time developer notifications)`),e.out.line(`notifications: ${t.notification_url??"?"} \u2014 Play's test notification ${t.verified_at?`arrived ${D(t.verified_at)}`:"not arrived yet"}`),t.connected_at&&e.out.line(`connected ${D(t.connected_at)}${t.connected_by?` by ${t.connected_by}`:""}`),t.action&&e.out.line(`To do: ${t.action}`)}var Q$={name:"purchases:google",summary:"the environment's Google Play purchases: the app they are made in, the topic its Play Console names, whether Play's test notification arrived",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/purchases/google");e.out.result(n,()=>Qh(e,n))}};async function eP(e,t){let n=(await qe(e,t.project_id)).filter(o=>o.deleted_at===null),s=n.filter(o=>o.platform==="android"),a=e.flags.app;if(a!==void 0){let o=Ye(n,a);if(o.platform!=="android")throw g(`${o.slug} is a ${o.platform} app: Google Play purchases are an Android app's`,`the project's Android apps: ${s.map(l=>l.slug).join(", ")||"none"}`);return o}let r=(await ka(e.io.cwd))?.apps.filter(o=>o.platform==="android").map(o=>o.id)??[],i=s.filter(o=>r.includes(o.id));if(i.length===1)return i[0];if(s.length===1)return s[0];throw s.length===0?new y("no_android_app",`${t.project_name} has no Android app`,{hint:"register it: palbase apps:create android <name>"}):g("name the Android app: --app <app>",`the project's Android apps: ${s.map(o=>o.slug).join(", ")}`)}var tP={name:"purchases:google:connect",summary:"sell on Google Play in one of the project's Android apps: Palbase makes the app's notifications topic and has it pushed to this environment",usage:"[--app <app>]",details:["The project's Play connection (palbase stores:google:connect) is the account Palbase reads and acknowledges the app's purchases as: its Play Console invites it with View financial data and Manage orders and subscriptions.","Then, in Play Console: Monetize with Play \u203A Monetization setup \u203A Real-time developer notifications, the topic this prints, Save, and Send test notification \u2014 palbase purchases:google says when it arrived."],flags:{app:{type:"string",value:"app",description:"the Android app (default: the one this checkout syncs, or the project's only one)"}},scope:"environment",async run(e){let t=await b(e),n=await eP(e,t);await _(e,t,`connect Google Play purchases for ${n.slug}`);let s={app_id:n.id},a=await m(e,t,"POST","/purchases/google",s,x());e.out.result(a,()=>{e.out.line(`Google Play purchases connected for ${a.package_name??n.slug} in ${q(t)}.`),Qh(e,a)})}},nP={name:"purchases:google:disconnect",summary:"stop taking the environment's Google Play purchases: Play's notifications stop reaching it, and the app's reports are refused (what was bought stays)",scope:"environment",async run(e){let t=await b(e);await _(e,t,"disconnect Google Play purchases"),await m(e,t,"DELETE","/purchases/google",void 0,x()),e.out.result({disconnected:!0},()=>e.out.line(`Disconnected Google Play purchases from ${q(t)}.`))}};async function sP(e,t){let n=(await qe(e,t.project_id)).find(s=>s.deleted_at===null&&s.environments.some(a=>a.ref===t.env_ref));return n===void 0?t.env_slug===Me:(await e.studio.get(`/environments/${t.env_ref}/apps/${n.id}/config`)).environment.production}async function aP(e,t){let n;try{let r=Va(t,e);if((await Jh(r)).size>V$)throw new y("not_a_certificate",`${e} is larger than Xcode's certificate`);n=await Ui(r)}catch(r){throw r instanceof y?r:new y("unreadable_file",`${e} could not be read: ${r instanceof Error?r.message:String(r)}`)}let s;try{s=new b$(n)}catch{throw new y("not_a_certificate",`${e} is not a certificate`,{hint:"the one Xcode saves: select the .storekit file, then Editor \u203A Save Public Certificate"})}let a=n.toString("latin1");return{certificate:a.includes("-----BEGIN CERTIFICATE-----")?a.trim():n.toString("base64"),x509:s}}var rP={name:"purchases:apple:xcode-certificate",summary:"take the purchases Xcode's StoreKit Testing signs with this certificate, as test purchases \u2014 never on production; --remove stops",usage:"<certificate.cer> | --remove",details:["Xcode saves the certificate: select the .storekit file (palbase purchases:storekit makes it), then Editor \u203A Save Public Certificate.","From then on what the app reports signed with it \u2014 Xcode's own purchases, on a developer's Mac \u2014 is a test purchase of the environment, with or without an In-App Purchase key; nothing of it is asked of Apple. Set again, it replaces the one before.","The production environment takes only what the App Store signed: it is refused there."],args:{min:0,max:1},flags:{remove:{type:"boolean",description:"stop taking Xcode's purchases (those taken stay)"}},scope:"environment",async run(e){let t=await b(e),n=e.flags.remove===!0,s=e.args[0];if(n===(s!==void 0))throw g("name the certificate file, or --remove","palbase purchases:apple:xcode-certificate StoreKitTestCertificate.cer");if(n){await _(e,t,"stop taking Xcode's StoreKit Testing purchases"),await m(e,t,"DELETE","/purchases/apple/xcode-certificate",void 0,x()),e.out.result({removed:!0},()=>e.out.line(`${q(t)} no longer takes Xcode's purchases; those it took stay.`));return}let a=await aP(s,e.io.cwd);if(await sP(e,t))throw new y("production_environment",`${t.project_name} \u203A ${t.env_name} is the project's production environment: it takes only purchases the App Store signed, never Xcode's`,{hint:"set the certificate on a test environment: palbase purchases:apple:xcode-certificate <file> -E <env>"});await _(e,t,`take the purchases Xcode signs with ${a.x509.subject.replaceAll(`
|
|
676
676
|
`,", ")}`);let r=await m(e,t,"PUT","/purchases/apple/xcode-certificate",{certificate:a.certificate},x());e.out.result(r,()=>{e.out.line(`${q(t)} takes the purchases Xcode signs with ${r.subject}, until ${D(r.not_after)} (sha256 ${r.fingerprint.slice(0,16)}\u2026).`),e.out.line("Run the app from Xcode and buy: each purchase is a test purchase of this environment.")})}},iP=new Set(["sandbox_access","xcode_certificate"]),oP={name:"purchases:doctor",summary:"whether the environment sells in the App Store now, check by check \u2014 the App Store Connect side and the purchases side \u2014 with the fix of each that fails",usage:"[--app <app>]",details:["Apple is asked now: the keys, the agreements, the app's App Store Connect record, where the App Store reports, the sandbox testers, each product's state, the catalog's last confirmation; and whether Xcode's purchases are taken.","It exits 1 while a check that is not informational fails: run it again until every line is \u2713."],flags:{app:{type:"string",value:"app",description:"the iOS app (default: the environment's one linked app)"}},scope:"environment",async run(e){let t=await b(e),n=e.flags.app!==void 0?await zi(e,t):void 0,s=n?`?${new URLSearchParams({app:n.id})}`:"",a=c=>c.then(h=>({value:h}),h=>({error:h})),[r,i]=await Promise.all([a(m(e,t,"GET",`/stores/apple/readiness${s}`)),a(m(e,t,"GET","/purchases/apple/readiness"))]),o=(c,h)=>{if(!(h instanceof I))throw h;return{module:c,id:`${c}_unanswered`,ok:!1,informational:!1,message:`The ${c} module could not answer: ${h.message}`,fix:h.status===503||h.status===502?"palbase purchases:doctor (again, in a moment)":""}},l=[..."value"in r?r.value.checks.map(c=>({module:"stores",id:c.id,ok:c.ok,informational:!1,message:c.message,fix:c.fix})):[o("stores",r.error)],..."value"in i?i.value.checks.map(c=>({module:"purchases",id:c.id,ok:c.ok,informational:iP.has(c.id),message:c.message,fix:c.fix})):[o("purchases",i.error)]],d=l.filter(c=>!c.ok&&!c.informational),p=d.length===0;return e.out.result({ready:p,checks:l,stores:"value"in r?r.value:null,purchases:"value"in i?i.value:null},()=>{let c=(h,f)=>{e.out.line(f);for(let w of l.filter(v=>v.module===h))e.out.line(` ${w.informational?"i":w.ok?"\u2713":"\u2717"} ${w.message}`),(!w.ok||w.informational)&&w.fix&&e.out.line(` \u2192 ${w.fix}`)};c("stores","App Store Connect (stores)"),c("purchases","In-App Purchases (purchases)"),e.out.line(""),e.out.line(p?`Ready: ${q(t)} sells in the App Store.`:`${d.length} of ${l.filter(h=>!h.informational).length} checks fail: fix each, then run palbase purchases:doctor again.`)}),p?void 0:1}},eg=[A$,R$,T$,C$,j$,O$,I$,L$,D$,N$,q$,G$,B$,H$,Y$,Z$,rP,Q$,tP,nP,oP];import{readdir as lP,readFile as pP,stat as dP}from"node:fs/promises";import{join as cP,resolve as uP}from"node:path";var hP=1e3,tg=900*1024,gP="palbase-argon2id";function mP(e,t){let n=e.trim();if(n.startsWith("{"))try{let s=JSON.parse(n);if(Array.isArray(s.users)){if(s.hash?.pepper!==void 0)throw g(`${t} holds a pepper: it goes on stdin, not in a file`);let a=s.hash&&typeof s.hash.algorithm=="string"?{algorithm:s.hash.algorithm,...typeof s.hash.recovery_code_prefix=="string"?{recovery_code_prefix:s.hash.recovery_code_prefix}:{}}:void 0;return{users:s.users,hash:a}}}catch(s){if(s instanceof Error&&s.name==="CliError")throw s}return{users:yP(e,t)}}function fP(e){let t=[],n=[],s=0;for(let a of e){let r=Buffer.byteLength(JSON.stringify(a))+1;if(r>tg)throw new y("user_too_large",`the user ${String(a.id??"")} is larger than one call takes (${r} bytes)`);(n.length===hP||s+r>tg)&&(t.push(n),n=[],s=0),n.push(a),s+=r}return n.length>0&&t.push(n),t}function yP(e,t){let n=e.trim();if(n==="")return[];if(n.startsWith("["))try{let s=JSON.parse(n);if(Array.isArray(s)&&s.every(a=>typeof a=="object"&&a!==null&&!Array.isArray(a)))return s}catch{}return n.split(`
|
|
677
677
|
`).flatMap((s,a)=>{if(s.trim()==="")return[];try{let r=JSON.parse(s);if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("not an object");return[r]}catch(r){throw g(`${t}:${a+1} is not a user (one JSON object a line): ${r instanceof Error?r.message:String(r)}`)}})}var ng={name:"auth:import",summary:"bring users over with what they sign in with: ids, addresses, password hashes, providers, second factors, passkeys, open sessions",usage:"<file|folder> [--pepper-stdin]",details:["A folder holds import bodies, one a file (users-0001.json\u2026, each without its pepper); a file holds one such body, JSON lines (a user each) or one JSON array.","Password and recovery-code hashes keep the source's pepper: give it on stdin with --pepper-stdin \u2014 never on the command line.","Users go 1000 a call. One refused does not stop the others; a user whose id or address the project has is refused, never overwritten."],args:{min:1,max:1},flags:{"pepper-stdin":{type:"boolean",description:"read the source's pepper from stdin"}},scope:"environment",async run(e){let t=await b(e),n=e.args[0],s=uP(e.io.cwd,n),a;try{a=(await dP(s)).isDirectory()?(await lP(s)).filter(c=>/^users-\d+\.json$/.test(c)).sort().map(c=>cP(s,c)):[s]}catch(c){throw g(`${n} could not be read: ${c instanceof Error?c.message:String(c)}`)}let r=[],i;for(let c of a){let h=mP(await pP(c,"utf8"),c);if(r.push(...h.users),h.hash){if(i&&JSON.stringify(i)!==JSON.stringify(h.hash))throw g(`${c} names another hash than the files before it`);i=h.hash}}if(r.length===0)throw g(`${n} holds no user`);let o=r.some(c=>c.password_hash!==void 0||Array.isArray(c.recovery_code_hashes)&&c.recovery_code_hashes.length>0),l;if(o){if(e.flags["pepper-stdin"]!==!0)throw g("these users carry password hashes: give the source's pepper with --pepper-stdin",`palbase auth:import ${n} --pepper-stdin < pepper.txt`);l={...i??{algorithm:gP},pepper:await Ce(e,"the source's pepper")}}await _(e,t,`bring ${r.length} user${r.length===1?"":"s"} into ${q(t)}'s auth`);let d={imported:0,errors:[],sessions_imported:0,sessions_skipped:0},p=0;for(let c of fP(r)){let h=await m(e,t,"POST","/admin/users/import",{...l?{hash:l}:{},users:c});d.imported+=h.imported,d.sessions_imported+=h.sessions_imported,d.sessions_skipped+=h.sessions_skipped;for(let f of h.errors??[])d.errors.push({...f,index:f.index+p});p+=c.length,p<r.length&&e.out.note(`${p} of ${r.length}\u2026`)}if(e.out.result(d,()=>{e.out.line(`Imported ${d.imported} of ${r.length} users (${d.sessions_imported} open sessions; ${d.sessions_skipped} ended ones left behind).`);for(let c of d.errors.slice(0,50))e.out.line(` ${c.id??`#${c.index+1}`}: ${c.error} \u2014 ${c.message}`);d.errors.length>50&&e.out.line(` \u2026and ${d.errors.length-50} more (--json lists every one)`)}),d.errors.length>0)throw new y("import_incomplete",`${d.errors.length} user${d.errors.length===1?" was":"s were"} not imported`)}};import{writeFile as sg}from"node:fs/promises";import{resolve as ag}from"node:path";var bP=/^usr_[A-Za-z0-9-]{1,64}$/;function Kt(e){if(!bP.test(e))throw g(`${JSON.stringify(e)} is not a user's id (usr_\u2026)`,"palbase auth:users:list shows them");return e}function qn(e,t,n,s){if(e===void 0)return;let a=Number(e);if(!Number.isInteger(a)||a<n||a>s)throw g(`--${t} is a whole number from ${n} to ${s}`);return a}function rg(e,t){if(e===void 0)return;let n=/^(\d+)([smhd])$/.exec(String(e));if(!n)throw g(`--${t} is a span: 30m, 2h, 7d`);return Number(n[1])*{s:1,m:60,h:3600,d:86400}[n[2]]}var St=e=>e?e.slice(0,16).replace("T"," "):"";function Bi(e){let t=[e.email_verified===!1?"unverified":"",e.banned?`banned${e.ban_reason?` (${C(e.ban_reason)})`:""}`:"",e.is_test?"test":"",e.has_mfa?"mfa":""].filter(Boolean);return`${C(e.id)} ${C(e.email??e.phone??"")}${t.length?` [${t.join(", ")}]`:""}`}var wP={name:"auth:users:list",summary:"the environment's users: id, email, verified, last sign-in; --banned for the banned alone",usage:"[--banned] [--limit <n>]",flags:{banned:{type:"boolean",description:"only the banned"},limit:{type:"string",value:"n",description:"how many at most (default 100)"}},scope:"environment",async run(e){let t=await b(e),n=qn(e.flags.limit,"limit",1,1e4)??100,s=[],a;do{let r=new URLSearchParams({limit:String(Math.min(100,n-s.length)),...a?{cursor:a}:{},...e.flags.banned===!0?{banned:"true"}:{}}),i=await m(e,t,"GET",`/admin/users?${r}`);s.push(...i.users),a=i.next_cursor||void 0}while(a&&s.length<n);e.out.result({users:s},()=>{s.length===0?e.out.line("No user yet."):e.out.table(s,[{header:"id",value:r=>r.id},{header:"email",value:r=>C(r.email??r.phone??"")},{header:"verified",value:r=>r.email_verified?"yes":"no"},{header:"last sign-in",value:r=>St(r.last_login_at)},{header:"notes",value:r=>[r.banned?"banned":"",r.is_test?"test":"",r.has_mfa?"mfa":""].filter(Boolean).join(", ")}])})}},vP={name:"auth:users:get",summary:"one user: their email and phone, how they sign in, whether verified or banned, their roles",usage:"<user-id>",args:{min:1,max:1},scope:"environment",async run(e){let t=await b(e),n=Kt(e.args[0]),[s,a]=await Promise.all([m(e,t,"GET",`/admin/users/${S(n)}`),m(e,t,"GET",`/admin/users/${S(n)}/roles`).then(r=>Array.isArray(r)?r:r.roles??[])]);e.out.result({...s,roles:a},()=>{e.out.line(Bi(s)),e.out.line(` signs in with ${s.providers?.length?C(s.providers.join(", ")):"a password"}; made ${C(St(s.created_at))}, last ${C(St(s.last_login_at))||"never"}`),e.out.line(` roles: ${a.length?a.map(C).join(", "):"none"}`)})}},_P={name:"auth:users:create",summary:"make a user with an email and a password (read from stdin, never the command line)",usage:"<email> [--verified]",args:{min:1,max:1},flags:{verified:{type:"boolean",description:"their email is taken as verified"}},scope:"environment",async run(e){let t=await b(e),n=await Ce(e,"the password");await _(e,t,`make the user ${e.args[0]}`);let s=await m(e,t,"POST","/admin/users",{email:e.args[0],password:n,...e.flags.verified===!0?{email_verified:!0}:{}},x());e.out.result(s,()=>e.out.line(`Made ${Bi(s)}.`))}};function Ha(e,t,n,s,a,r={}){return{name:e,summary:t,usage:"<user-id>"+(r&&"reason"in r?" [--reason <text>]":""),args:{min:1,max:1},flags:r,scope:"environment",async run(i){let o=await b(i),l=Kt(i.args[0]);await _(i,o,n(l)),await s(i,o,l),i.out.result({user:l,done:e},()=>i.out.line(a(l)))}}}var kP=Ha("auth:users:delete","delete a user: their account, sessions and sign-ins go; backends hear user.deleted",e=>`delete the user ${e}`,(e,t,n)=>m(e,t,"DELETE",`/admin/users/${S(n)}`),e=>`Deleted ${e}.`),SP=Ha("auth:users:ban","ban a user: their sessions end and they cannot sign in until unbanned",e=>`ban the user ${e}`,(e,t,n)=>m(e,t,"POST",`/admin/users/${S(n)}/ban`,e.flags.reason===void 0?{}:{reason:e.flags.reason}),e=>`Banned ${e}.`,{reason:{type:"string",value:"text",description:"why (kept with the ban)"}}),$P=Ha("auth:users:unban","let a banned user sign in again",e=>`unban the user ${e}`,(e,t,n)=>m(e,t,"POST",`/admin/users/${S(n)}/unban`,{}),e=>`${e} may sign in again.`),PP=Ha("auth:users:sign-out","end every session of a user's: each device signs in again",e=>`end every session of ${e}`,(e,t,n)=>m(e,t,"POST",`/admin/users/${S(n)}/sessions/revoke-all`,{}),e=>`Every session of ${e} is ended.`),AP=/^[a-z][a-z0-9_-]{0,63}$/,ig={type:"string",value:"name",description:"the machine whose backend has the template (default: the environment's only one)"};function og(e){if(typeof e!="string"||!AP.test(e))throw g(`${JSON.stringify(e??"")} is not a template's name: lowercase letters, digits, _ and -, a letter first`,"the backend's @TestUserTemplate({ name }) says it");return e}var lg=e=>e instanceof I&&e.status===403&&/scope|closed/.test(e.code);async function pg(e,t){let{machines:n}=await m(e,t,"GET","/machines?limit=100");return n.map(s=>s.name)}async function dg(e,t){let n=e.flags.machine;if(n!==void 0)return n;let s;try{s=await pg(e,t)}catch(a){throw lg(a)?g("this token does not list the machines: name the one whose backend has the template","--machine <name>"):a}if(s.length===1)return s[0];throw s.length===0?new y("no_machine","the environment has no machine: a template is written by a backend running on one",{hint:"palbase machines:create"}):g("which machine? name it",`the environment's machines: ${s.join(", ")} \u2014 --machine <name>`)}function cg(e){if(e.error)return` template ${C(e.name)} not written: ${C(e.error)}`;let t=Object.entries(e.rows??{}).map(([n,s])=>`${C(n)} ${s}`);return` template ${C(e.name)}${e.again?" (written before)":""}: ${t.length>0?t.join(", "):"no rows"}`}var RP={name:"auth:test-users:list",summary:"the environment's test users (made with its secret key; their tokens say test: true)",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/admin/test-users"),s=Array.isArray(n)?n:n.users;e.out.result({users:s},()=>{s.length===0&&e.out.line("No test user yet: palbase auth:test-users:create makes some.");for(let a of s)e.out.line(Bi(a))})}},TP={name:"auth:test-users:create",summary:"make test users for a suite: their passwords, and with --token an access token each, shown this once; --template has the backend write the data a screen needs for each",usage:"[--count <n>] [--token] [--template <name> [--machine <name>]]",details:["A test user signs in as any user does; its tokens say test: true, so a backend can tell them apart. --token gives each an access token at once, for a test that calls the backend as that user.","--template <name> has the backend write the data of its @TestUserTemplate({ name }) for each user \u2014 its seed for one account, in its own code (palbase/backend), run on the machine --machine names or the environment's only one, for test users alone. Each user is answered with the rows written, by table, or why its template was not (no_template, not_a_test_user, template_failed, template_timeout, machine_unavailable): the user is made all the same, and the command then ends with 1. At most 10 users at once with a template.","palbase auth:test-users:template writes another template for a user made with one; palbase auth:test-users:delete has the template undone before the user is deleted."],flags:{count:{type:"string",value:"n",description:"how many (default 1, at most 100; 10 with --template)"},token:{type:"boolean",description:"an access token for each, too"},template:{type:"string",value:"name",description:"a @TestUserTemplate of the backend's: its data written for each user"},machine:ig},scope:"environment",async run(e){let t=await b(e);if(e.flags.template===void 0){if(e.flags.machine!==void 0)throw g("--machine names where --template is written: give --template too");let o=qn(e.flags.count,"count",1,100)??1;await _(e,t,`make ${o} test user(s)`);let l=await m(e,t,"POST","/admin/test-users",{count:o,with_tokens:e.flags.token===!0},x());e.out.result(l,()=>{for(let d of l.users)e.out.line(`${C(d.user_id)} ${C(d.email??"")} password ${C(d.password??"")}`),d.access_token&&e.out.line(` access token: ${C(d.access_token)}`);e.out.line("Shown this once.")});return}let n=og(e.flags.template),s=qn(e.flags.count,"count",1,10)??1,a=await dg(e,t);await _(e,t,`make ${s} test user(s) with ${a}'s template ${n}`);let r=await m(e,t,"POST",`/machines/${S(a)}/test-users`,{count:s,template:n,with_tokens:e.flags.token===!0},x()),i=r.users.filter(o=>o.template?.error);if(e.out.result({machine:a,...r},()=>{for(let o of r.users)e.out.line(`${C(o.user_id)} ${C(o.email??"")} password ${C(o.password??"")}`),o.access_token&&e.out.line(` access token: ${C(o.access_token)}`),e.out.line(cg(o.template??{name:n,error:"the platform said nothing of it"}));e.out.line("Shown this once.")}),i.length>0)return 1}},EP={name:"auth:test-users:template",summary:"have the backend write a @TestUserTemplate's data for a test user made with a template \u2014 the same one again answers what it wrote then",usage:"<user-id> <template> [--machine <name>]",details:["The user is one auth:test-users:create --template made on that machine: a test user is the machine's that made it. Asked again for a template it wrote, the platform answers what it wrote then, and the backend is not called."],args:{min:2,max:2},flags:{machine:ig},scope:"environment",async run(e){let t=await b(e),n=Kt(e.args[0]),s=og(e.args[1]),a=await dg(e,t);await _(e,t,`write ${a}'s template ${s} for the test user ${n}`);let r=await m(e,t,"POST",`/machines/${S(a)}/test-users/${S(n)}/templates/${S(s)}`);if(e.out.result({user:n,machine:a,template:r},()=>e.out.line(`${C(n)}${cg(r)}`)),r.error)return 1}},CP={name:"auth:test-users:delete",summary:"delete a test user \u2014 one made with a template has the backend undo it first",usage:"<user-id> [--machine <name>]",details:["A test user made with --template is deleted through the machine that made it: the backend's undo of each template runs first \u2014 a template with none leaves its data to user.deleted (what ownedByUser() owns goes with the user) \u2014 and an undo that fails keeps the user, for the delete to be asked again. Any other test user is deleted at once.","Without --machine the environment's machines are asked which made the user; a token that does not list the machines deletes it at once, its templates left to user.deleted."],args:{min:1,max:1},flags:{machine:{type:"string",value:"name",description:"the machine that made the user with a template (default: whichever did)"}},scope:"environment",async run(e){let t=await b(e),n=Kt(e.args[0]);await _(e,t,`delete the test user ${n}`);let s=e.flags.machine,a=[],r=!1;if(s!==void 0)a=[s];else try{a=await pg(e,t)}catch(o){if(!lg(o))throw o;r=!0}let i;for(let o of a)try{await m(e,t,"DELETE",`/machines/${S(o)}/test-users/${S(n)}`),i=o;break}catch(l){if(l instanceof I&&l.status===404)continue;throw l}if(i===void 0&&s!==void 0)throw new y("not_found",`${n} is no test user ${s} made with a template`,{hint:"leave --machine out: the machine that made it is found, and a test user made without a template is deleted at once"});i===void 0&&await m(e,t,"DELETE",`/admin/test-users/${S(n)}`),e.out.result({user:n,done:"auth:test-users:delete",...i!==void 0?{machine:i}:{}},()=>{r&&e.out.note("this token does not list the machines: no template's undo was asked (user.deleted forgets what the user owned) \u2014 --machine <name> asks it"),e.out.line(i!==void 0?`Deleted the test user ${n}: ${i}'s backend undid its templates first.`:`Deleted the test user ${n}.`)})}},jP={name:"flags:users:get",summary:"every flag as one user gets it now \u2014 their own values, the targeting, the defaults \u2014 as their client reads them",usage:"<user-id>",args:{min:1,max:1},scope:"environment",async run(e){let t=await b(e),n=Kt(e.args[0]),s=await m(e,t,"GET",`/flags/users/${S(n)}`);e.out.result({user:n,values:s.values},()=>{let a=Object.keys(s.values??{});a.length===0&&e.out.line("The environment has no flag yet.");for(let r of a.sort())e.out.line(`${C(r)} = ${JSON.stringify(s.values[r])}`)})}},OP={name:"flags:users:set",summary:"give a user a flag's value of their own, whatever its default and targeting say",usage:"<user-id> <key> <value>",args:{min:3,max:3},scope:"environment",async run(e){let t=await b(e),n=Kt(e.args[0]),s=e.args[1],a=at(e.args[2]);await _(e,t,`give ${n} the value ${JSON.stringify(a)} of ${s}`),await m(e,t,"PUT",`/flags/users/${S(n)}/${S(s)}`,{value:a}),e.out.result({user:n,key:s,value:a},()=>e.out.line(`${n} gets ${s} = ${JSON.stringify(a)}.`))}},xP={name:"flags:users:unset",summary:"take a user's own value of a flag away, or every one with no key: the flag decides for them again",usage:"<user-id> [key]",args:{min:1,max:2},scope:"environment",async run(e){let t=await b(e),n=Kt(e.args[0]),s=e.args[1];await _(e,t,s?`take ${n}'s own value of ${s} away`:`take every value of ${n}'s own away`),await m(e,t,"DELETE",s?`/flags/users/${S(n)}/${S(s)}`:`/flags/users/${S(n)}`),e.out.result({user:n,key:s??null},()=>e.out.line(s?`${n} gets ${s} as the flag says.`:`${n} gets every flag as it says.`))}};function IP(e){let t=e===void 0?[]:Array.isArray(e)?e:[String(e)];if(t.length===0)return;let n={};for(let s of t){let a=s.indexOf("=");if(a<=0)throw g(`--var is name=value, not ${JSON.stringify(s)}`);n[s.slice(0,a)]=s.slice(a+1)}return n}var MP={name:"notify:send",summary:"send an email or a text through the environment's providers: written here, or a template with its variables",usage:"<email|sms> <to> [--template <slug> --var name=value\u2026] [--subject <s> --text <t> --html <h>] [--body <text>]",details:["A message is sent as a backend sends it (POST /notify/email, /notify/sms): its delivery is in palbase notify:messages."],args:{min:2,max:2},flags:{template:{type:"string",value:"slug",description:"a template of the environment (notify:templates:list)"},var:{type:"string",value:"name=value",multiple:!0,description:"a template's variable (again for each)"},locale:{type:"string",value:"locale",description:"the template's locale"},subject:{type:"string",value:"text",description:"an email's subject"},text:{type:"string",value:"text",description:"an email's text"},html:{type:"string",value:"html",description:"an email's HTML"},body:{type:"string",value:"text",description:"a text's words"}},scope:"environment",async run(e){let[t,n]=e.args;if(t!=="email"&&t!=="sms")throw g("notify:send email <to> or notify:send sms <to>");let s=await b(e),a=e.flags,r=IP(a.var),i={to:n,...a.template?{template:a.template}:{},...r?{variables:r}:{},...a.locale?{locale:a.locale}:{}};if(t==="email"){if(!a.template&&(a.subject===void 0||a.text===void 0&&a.html===void 0))throw g("an email is a --template, or a --subject with --text or --html");Object.assign(i,a.subject!==void 0?{subject:a.subject}:{},a.text!==void 0?{text:a.text}:{},a.html!==void 0?{html:a.html}:{})}else{if(!a.template&&a.body===void 0)throw g("a text is a --template or a --body");a.body!==void 0&&(i.body=a.body)}await _(e,s,`send ${t==="email"?"an email":"a text"} to ${n}`);let o=await m(e,s,"POST",`/notify/${t}`,i,x());e.out.result(o,()=>e.out.line(o.skipped?`Not sent to ${C(n)}: the user does not take this category (${C(o.message_id??"")}).`:`Queued to ${C(n)} (${C(o.message_id??"")}): palbase notify:messages says what became of it.`))}},LP={name:"notify:messages",summary:"the messages the environment sent, newest first, and what became of each",usage:"[--limit <n>]",flags:{limit:{type:"string",value:"n",description:"how many (default 20)"}},scope:"environment",async run(e){let t=await b(e),n=qn(e.flags.limit,"limit",1,100)??20,s=await m(e,t,"GET",`/notify/messages?limit=${n}`);e.out.result(s,()=>{s.messages.length===0?e.out.line("Nothing sent yet."):e.out.table(s.messages,[{header:"sent",value:a=>St(a.created_at)},{header:"channel",value:a=>a.channel},{header:"to",value:a=>C(a.recipient_hint??"")},{header:"template",value:a=>C(a.template??"")},{header:"status",value:a=>`${a.status}${a.error?` (${C(a.error)})`:""}`},{header:"id",value:a=>a.id}])})}},DP={name:"notify:deliverability",summary:"whether each channel can send now, through which providers, and what to do when it cannot",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/notify/deliverability");e.out.result(n,()=>e.out.table(n.channels,[{header:"channel",value:s=>s.channel},{header:"sends",value:s=>s.ok?"yes":"no"},{header:"via",value:s=>C(s.via.join(" \u2192 "))||"-"},{header:"reports",value:s=>s.reports?"yes":"no"},{header:"why / what to do",value:s=>C([s.because,s.remedy].filter(Boolean).join(" \u2014 "))}]))}},NP={name:"auth:audit",summary:"who did what in the environment's auth, newest first: sign-ins, changes to users and settings, admin actions",usage:"[--event <type>] [--limit <n>]",flags:{event:{type:"string",value:"type",description:"only this kind of event (user.login, admin.user.update\u2026)"},limit:{type:"string",value:"n",description:"how many at most (default 50)"}},scope:"environment",async run(e){let t=await b(e),n=qn(e.flags.limit,"limit",1,1e4)??50,s=[],a;do{let r=new URLSearchParams({limit:String(Math.min(100,n-s.length)),...a?{cursor:a}:{},...typeof e.flags.event=="string"?{event_type:e.flags.event}:{}}),i=await m(e,t,"GET",`/admin/audit?${r}`);s.push(...i.events),a=i.next_cursor||void 0}while(a&&s.length<n);e.out.result({events:s},()=>{s.length===0?e.out.line("Nothing recorded yet."):e.out.table(s,[{header:"when",value:r=>St(r.created_at)},{header:"event",value:r=>C(r.event_type)},{header:"result",value:r=>C(r.result??"")},{header:"by",value:r=>C(r.actor?.email??r.actor?.user_id??"")},{header:"from",value:r=>C(r.actor?.ip??"")},{header:"on",value:r=>C([r.target_type,r.target_id].filter(Boolean).join(" "))}])})}},qP={name:"auth:audit:verify",summary:"whether the audit trail is whole: each event chained to the one before, none changed or taken out",scope:"environment",async run(e){let t=await b(e),n=await m(e,t,"GET","/admin/audit/verify");if(!n.valid){let s=n.broken_at?` at event ${C(n.broken_at.event_id??"?")} (number ${(n.broken_at.index??0)+1} of ${n.total}, oldest first)`:"";throw new y("audit_trail_broken",`the audit trail is broken${s}: an event there was changed or taken out`)}e.out.result(n,()=>e.out.line(`The audit trail is whole: ${n.total} events, each chained to the one before.`))}},GP={name:"auth:audit:export",summary:"the audit trail as a file: JSON lines or CSV, a span of it, one kind of event",usage:"[--format ndjson|csv] [--from <time>] [--to <time>] [--event <type>] [--out <file>]",flags:{format:{type:"string",value:"ndjson|csv",description:"JSON lines (default) or CSV"},from:{type:"string",value:"time",description:"from this time on (RFC 3339: 2026-10-01T00:00:00Z)"},to:{type:"string",value:"time",description:"up to this time"},event:{type:"string",value:"type",description:"only this kind of event"},out:{type:"string",value:"file",description:"write it here instead of to standard output"}},scope:"environment",async run(e){let t=e.flags.format??"ndjson";if(t!=="ndjson"&&t!=="csv")throw g("--format is ndjson or csv");for(let i of["from","to"]){let o=e.flags[i];if(o!==void 0&&Number.isNaN(Date.parse(String(o))))throw g(`--${i} is a time: 2026-10-01T00:00:00Z`)}let n=await b(e),s=new URLSearchParams({format:String(t)});for(let[i,o]of[["from","from"],["to","to"],["event","event_type"]])typeof e.flags[i]=="string"&&s.set(o,e.flags[i]);let a=await m(e,n,"GET",`/admin/audit/export?${s}`),r=typeof a=="string"?a:t==="ndjson"&&Array.isArray(a)?a.map(i=>JSON.stringify(i)).join(`
|
|
678
678
|
`):JSON.stringify(a);typeof e.flags.out=="string"?(await sg(ag(e.io.cwd,e.flags.out),r.endsWith(`
|
package/dist/refuse/ai.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
2
|
throw new Error("palbase/ai is for servers: it speaks with the project secret key, which never goes into a browser or a mobile app \u2014 call your backend, and let it ask the model");
|
package/dist/refuse/backend.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
2
|
throw new Error("palbase/backend is a server framework: it runs on your backend, never in a browser or a mobile app \u2014 apps use palbase/web (or the native SDKs)");
|
package/dist/refuse/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
2
|
throw new Error("palbase/server is for servers: its secret key never goes into a browser or a mobile app \u2014 apps use palbase/web (or the native SDKs) with the publishable key");
|
package/dist/server/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import{a as e,b as f,c as g,d as h,e as i,f as j,g as k,h as l,i as m,j as n,k as o,l as p,m as q,n as r,o as s,p as t,q as u,r as v}from"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import{a as e,b as f,c as g,d as h,e as i,f as j,g as k,h as l,i as m,j as n,k as o,l as p,m as q,n as r,o as s,p as t,q as u,r as v}from"../chunks/Q3S76MYJ.js";import{a as c,b as d}from"../chunks/26UTOC32.js";import"../chunks/EEWP43KI.js";import{m as b}from"../chunks/7WIMZTYB.js";import"../chunks/R3EIGCWV.js";import{b as a}from"../chunks/KNPQNBC2.js";import"../chunks/BUAGC2PT.js";import"../chunks/43VM626B.js";export{l as AuthUnavailable,b as EVENT_TYPES,c as FieldError,d as Fields,a as PalbaseError,q as ReservationSettled,k as TokenError,n as TokenVerifier,f as WebhookSignatureError,e as createServer,p as isEntitled,v as loadSecrets,s as pages,u as platformOwn,t as processOwn,o as requestAddress,m as sessionStanding,i as signatureHeader,h as verifySignature,g as verifyWebhook,j as verifyWebhookRequest,r as withReservation};
|
package/dist/server/testing.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import{a,b,c,d,e}from"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import{a,b,c,d,e}from"../chunks/EWBRYA7P.js";import"../chunks/26UTOC32.js";import"../chunks/BUAGC2PT.js";import"../chunks/43VM626B.js";export{b as TestApiError,e as api,c as createTestApi,a as testFields,d as testRun};
|
package/dist/web/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import{a as h,b as g,c as C,d as w,e as v,f as b,g as E,h as T,i as P,j as S,k as x,l as k,m as R,n as I,o as L,p as M,q as G}from"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import{a as h,b as g,c as C,d as w,e as v,f as b,g as E,h as T,i as P,j as S,k as x,l as k,m as R,n as I,o as L,p as M,q as G}from"../chunks/FVLHT7UQ.js";import{a as V,b as O,f as A}from"../chunks/V2FEPVVD.js";import"../chunks/ZZG7ICDG.js";import"../chunks/3453XVQL.js";import"../chunks/3VY3ZZCI.js";import"../chunks/LYEEW4UG.js";import"../chunks/7WIMZTYB.js";import"../chunks/R3EIGCWV.js";import{b as u}from"../chunks/KNPQNBC2.js";import"../chunks/BUAGC2PT.js";import"../chunks/43VM626B.js";var D="oai-events";async function N(e){if(typeof e?.start!="function")throw new TypeError("connectVoice: start(sdp) is the round trip to the app's backend, which starts the voice (chats.voice.start) and answers { id, sdp }");let o=e.rtc??B(),t=e.signal;t?.throwIfAborted();let s=e.audio??F(),c=e.microphone===void 0,a=e.microphone??await o.getUserMedia({audio:!0});if(t?.aborted){if(c)for(let r of a.getTracks())r.stop();throw t.reason}let n=new o.RTCPeerConnection,p=!1,d=()=>{if(!p){if(p=!0,f.close(),n.close(),c)for(let r of a.getTracks())r.stop();s.srcObject!==null&&(s.srcObject=null)}};n.addEventListener("track",r=>{s.srcObject=r.streams[0]??new MediaStream([r.track]),s.play?.()?.catch?.(()=>{})}),n.addEventListener("connectionstatechange",()=>e.onState?.(n.connectionState));for(let r of a.getAudioTracks())n.addTrack(r,a);let f=n.createDataChannel(D);f.addEventListener("message",r=>{let i=U(r.data);i!==void 0&&e.onError?.(i)});let y=()=>d();t?.addEventListener("abort",y,{once:!0});try{await n.setLocalDescription(await n.createOffer()),t?.throwIfAborted(),await H(n,e.iceTimeoutMs??1e4,t);let r=n.localDescription?.sdp;if(!r)throw new Error("connectVoice: the peer connection made no offer");let i=await e.start(r);if(t?.throwIfAborted(),typeof i?.id!="string"||i.id===""||typeof i.sdp!="string"||i.sdp==="")throw new TypeError("connectVoice: start answered no voice \u2014 it is { id, sdp }, chats.voice.start's answer as the backend got it");return await n.setRemoteDescription({type:"answer",sdp:i.sdp}),t?.removeEventListener("abort",y),{id:i.id,peer:n,audio:s,microphone:a,get muted(){return a.getAudioTracks().every(l=>!l.enabled)},mute(l){for(let m of a.getAudioTracks())m.enabled=!l},close:d}}catch(r){throw t?.removeEventListener("abort",y),d(),t?.aborted?t.reason:r}}function H(e,o,t){return e.iceGatheringState==="complete"?Promise.resolve():t?.aborted?Promise.reject(t.reason):new Promise((s,c)=>{let a=()=>{clearTimeout(d),e.removeEventListener("icegatheringstatechange",n),t?.removeEventListener("abort",p)},n=()=>{e.iceGatheringState==="complete"&&(a(),s())},p=()=>{a(),c(t?.reason)},d=setTimeout(()=>{a(),c(new Error(`connectVoice: the network candidates were not gathered in ${o} ms`))},o);e.addEventListener("icegatheringstatechange",n),t?.addEventListener("abort",p,{once:!0})})}function U(e){if(typeof e=="string")try{let o=JSON.parse(e);return o.type!=="error"?void 0:typeof o.error?.message=="string"?o.error.message:typeof o.error?.code=="string"?o.error.code:"the voice failed"}catch{return}}function B(){let e=globalThis;if(typeof e.RTCPeerConnection!="function"||typeof e.navigator?.mediaDevices?.getUserMedia!="function")throw new TypeError("connectVoice: this runtime has no WebRTC (RTCPeerConnection, navigator.mediaDevices): a chat's voice is a browser's");let o=e.navigator.mediaDevices;return{RTCPeerConnection:e.RTCPeerConnection,getUserMedia:t=>o.getUserMedia(t)}}function F(){let e=globalThis.document;if(!e)throw new TypeError("connectVoice: give audio \u2014 the element the voice plays in \u2014 where there is no document");let o=e.createElement("audio");return o.autoplay=!0,o}export{E as Analytics,V as BackendError,w as Channel,T as Consent,M as Flags,A as GatewayError,x as InApp,P as Inbox,b as Installation,R as Notify,u as PalbaseError,I as Purchases,v as RealtimeSocket,S as SESSION_START,h as SessionChangedError,k as WebPush,N as connectVoice,G as createClient,O as isBackendError,C as localStore,g as memoryStore,L as redemptionToken};
|
package/dist/web/message.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import{a as i}from"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import{a as i}from"../chunks/3453XVQL.js";import{a as e,b as s,c as t,d as p,e as o,f as a,h as r,j as n,k as y,p as M,q as l,t as g}from"../chunks/3VY3ZZCI.js";import"../chunks/43VM626B.js";export{n as MESSAGE_COMPONENTS,a as MESSAGE_ICONS,r as MESSAGE_SCHEMA,s as assetId,y as countdownValues,e as fontFamily,o as htmlDocument,t as prepareAssets,i as presentMessage,g as renderMessage,M as resolveMessage,l as resolveMessageWithPaths,p as unzip};
|
package/dist/web/palbe.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import{c as e,d as r,i as t}from"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import{c as e,d as r,i as t}from"../chunks/V2FEPVVD.js";import"../chunks/LYEEW4UG.js";import"../chunks/R3EIGCWV.js";import"../chunks/KNPQNBC2.js";import"../chunks/BUAGC2PT.js";import"../chunks/43VM626B.js";export{e as RESERVED_CLIENT_NAMES,t as __registerBackend,r as isClientName};
|
package/dist/web/paywall.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import{a as f,b as d,c as m,d as u,e as A,f as c,g as R}from"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import{a as f,b as d,c as m,d as u,e as A,f as c,g as R}from"../chunks/ZZG7ICDG.js";import{a as e,b as a,c as t,d as l,e as r,f as o,g as p,i as y,k as s,l as P,n,o as i,s as w}from"../chunks/3VY3ZZCI.js";import"../chunks/KNPQNBC2.js";import"../chunks/BUAGC2PT.js";import"../chunks/43VM626B.js";export{y as PAYWALL_COMPONENTS,o as PAYWALL_ICONS,p as PAYWALL_SCHEMA,c as Paywall,a as assetId,f as attributesHeader,s as countdownValues,R as defaultPaywall,e as fontFamily,u as formatPeriod,m as formatPrice,r as htmlDocument,A as packageValues,P as packagesOf,d as parseDuration,t as prepareAssets,w as renderPaywall,n as resolvePaywall,i as resolvePaywallWithPaths,l as unzip};
|
package/dist/web/push-sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
2
|
"use strict";(()=>{function f(e,s){if(e)try{let i=new URL(e,s);if(i.protocol==="https:"||i.protocol==="http:"&&(i.hostname==="localhost"||i.hostname==="127.0.0.1"))return i.href}catch{}}function g(e){let s=new URL(e.location.href).searchParams,i=s.get("url")??"",l=s.get("key")??"",r=(n,t)=>!n||!i?Promise.resolve():e.fetch(`${i.replace(/\/$/,"")}/notify/receipts`,{method:"POST",headers:{"content-type":"application/json",apikey:l},body:JSON.stringify({receipts:[{receipt:n,status:t}]}),keepalive:!0}).catch(()=>{}),a=async(n,t)=>{for(let o of await e.clients.matchAll({type:"window",includeUncontrolled:!0}))o.postMessage({type:"palbase.push",event:n,url:t.url,data:t.data})};e.addEventListener("push",(n=>{let t={};try{t=n.data?.json()??{}}catch{}let o={url:t.url,receipt:t.receipt,data:t.data};n.waitUntil(e.registration.showNotification(t.title??"",{body:t.body,image:t.image,tag:t.tag,data:o}).then(()=>Promise.all([r(t.receipt,"delivered"),a("received",o)])))})),e.addEventListener("notificationclick",(n=>{n.notification.close();let t=n.notification.data??{},o=f(t.url,e.location.origin),c=async()=>{if(!o)return;let d=(await e.clients.matchAll({type:"window",includeUncontrolled:!0})).find(u=>u.url===o);d?await d.focus():await e.clients.openWindow(o)};n.waitUntil(Promise.all([r(t.receipt,"opened"),c(),a("opened",t)]))})),e.addEventListener("notificationclose",(n=>{n.waitUntil(r(n.notification.data?.receipt,"dismissed"))}))}typeof self<"u"&&typeof self.registration=="object"&&!("document"in self)&&g(self);})();
|
package/dist/web/ssr.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import{q as f}from"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import{q as f}from"../chunks/FVLHT7UQ.js";import"../chunks/V2FEPVVD.js";import"../chunks/ZZG7ICDG.js";import"../chunks/3453XVQL.js";import"../chunks/3VY3ZZCI.js";import"../chunks/LYEEW4UG.js";import"../chunks/7WIMZTYB.js";import"../chunks/R3EIGCWV.js";import"../chunks/KNPQNBC2.js";import"../chunks/BUAGC2PT.js";import"../chunks/43VM626B.js";var C=Object.freeze({"Cache-Control":"private, no-cache, no-store, must-revalidate, max-age=0",Expires:"0",Pragma:"no-cache"}),x={path:"/",sameSite:"lax",secure:!0,httpOnly:!1,maxAge:400*24*60*60},u=3180,k=5*6e4;function d(t){let e=/^pb_([a-z0-9]+)_c/.exec(t)?.[1];if(e===void 0)throw new TypeError("publishableKey is the project publishable key (pb_<ref>_c\u2026)");return`pb-${e}-session`}var y=t=>{let e=new TextEncoder().encode(JSON.stringify(t)),o="";for(let s of e)o+=String.fromCharCode(s);return btoa(o).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")},b=t=>{try{let e=atob(t.replace(/-/g,"+").replace(/_/g,"/")),o=JSON.parse(new TextDecoder().decode(Uint8Array.from(e,s=>s.charCodeAt(0))));return typeof o.accessToken!="string"||typeof o.refreshToken!="string"||typeof o.expiresAt!="number"||typeof o.userId!="string"?null:o}catch{return null}};function m(t,e,o={},s=!1){let c={...x,...o},g=async()=>new Map((await e.getAll()).map(n=>[n.name,n.value])),S=n=>n===t||n.startsWith(`${t}.`)&&/^\d+$/.test(n.slice(t.length+1)),h=async n=>{if(!e.setAll)return;let i=n===null?[]:n.length<=u?[[t,n]]:Array.from({length:Math.ceil(n.length/u)},(r,l)=>[`${t}.${l}`,n.slice(l*u,(l+1)*u)]),p=new Set(i.map(([r])=>r)),a=i.map(([r,l])=>({name:r,value:l,options:c}));for(let r of(await g()).keys())S(r)&&!p.has(r)&&a.push({name:r,value:"",options:{...c,maxAge:0}});a.length>0&&await e.setAll(a,{...C})};return{async load(){let n=await g(),i=n.get(t);if(i===void 0){let p="";for(let a=0;n.has(`${t}.${a}`);a++)p+=n.get(`${t}.${a}`);i=p}return i?b(i):null},save:n=>h(y(n)),clear:()=>h(null),shared:s}}function A(t){let{cookies:e,cookieOptions:o,...s}=t,c=t.refresh??(e.setAll?"before":"never");return f({...s,refresh:c,storage:m(d(t.publishableKey),e,o)})}function O(){let t=()=>{let e=globalThis.document;if(!e)throw new Error("documentCookies: there is no document here \u2014 a server gives its request's cookies to createServerClient");return e};return{getAll:()=>t().cookie.split(";").map(e=>e.trim()).filter(e=>e.includes("=")).map(e=>{let o=e.indexOf("=");return{name:e.slice(0,o),value:e.slice(o+1)}}),setAll(e){for(let o of e){let s=o.options,c=[`Path=${s.path??"/"}`,s.domain?`Domain=${s.domain}`:"",s.maxAge!==void 0?`Max-Age=${s.maxAge}`:"",s.expires?`Expires=${s.expires.toUTCString()}`:"",s.sameSite?`SameSite=${s.sameSite.charAt(0).toUpperCase()}${s.sameSite.slice(1)}`:"",s.secure?"Secure":""].filter(Boolean);t().cookie=[`${o.name}=${o.value}`,...c].join("; ")}}}}function $(t){let{cookieOptions:e,...o}=t,s=globalThis.location?.protocol==="https:";return f({...o,storage:m(d(t.publishableKey),O(),{secure:s,...e},!0)})}async function v(t){let e=await t.auth.currentSession();if(!e||(e.expiresAt-Date.now()<=k&&await t.auth.refreshSession().catch(()=>{}),!await t.auth.currentSession()))return null;try{return(await t.auth.api.verifyTokenUser()).data}catch(o){if(o.status===401)return null;throw o}}export{C as NO_STORE_HEADERS,x as SESSION_COOKIE_OPTIONS,m as cookieSessionStore,$ as createBrowserClient,A as createServerClient,O as documentCookies,v as keepSession,d as sessionCookieName};
|
package/dist/web/vite.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/*! palbase 0.11.
|
|
2
|
-
import"../chunks/
|
|
1
|
+
/*! palbase 0.11.1 — Palbase SDK License (see LICENSE) */
|
|
2
|
+
import"../chunks/43VM626B.js";import{execFile as $}from"node:child_process";import{existsSync as y,mkdtempSync as k,readFileSync as S,rmSync as x,writeFileSync as P}from"node:fs";import{createRequire as O}from"node:module";import{tmpdir as W}from"node:os";import{dirname as g,join as o,relative as u,resolve as v,sep as d}from"node:path";var E=/\.(?:ts|js|mts|mjs)$/;function F(n){if(!/^[a-z][a-z0-9-]*$/.test(n.app))throw new TypeError(`palbase({ app }): ${JSON.stringify(n.app)} is an app's slug (palbase apps:list)`);let c=process.cwd(),p="",i="",f=()=>v(c,n.dir??"palbase"),m=()=>{let e=n.environments[p];if(!e)throw new Error(`palbase: the mode "${p}" is not built against any environment \u2014 say which in vite.config: palbase({ environments: { ${JSON.stringify(p)}: '<environment slug>' } })`);return e};return{name:"palbase",enforce:"pre",config(e,s){p=s.mode,i=s.command??"";let r=e.build?.rollupOptions?.onwarn;return{build:{rollupOptions:{onwarn(t,a){let l=String(t.exporter??t.id??"");if(t.code==="MISSING_EXPORT"&&l.includes(`${d}environments${d}`))throw new Error(`palbase: ${t.message} \u2014 ${m()}'s backend has no such route (palbase apps:sync brings what it has)`);r?r(t,a):a(t)}}}}},configResolved(e){c=e.root,p=e.mode,i=e.command??i},async buildStart(){i!=="build"||n.typecheck===!1||await j(c,m(),n.tsconfig)},resolveId(e,s){if(!s)return null;let r=f();if(e==="palbase"||e.startsWith("palbase/")){let w=u(r,s);return w.startsWith("..")||w===""?null:this.resolve(e,o(c,"index.html"),{skipSelf:!0})}if(!(e.startsWith(".")||e.startsWith("/")))return null;let t=v(g(s),e);if(E.test(t)||(t+=".ts"),g(t)!==r&&g(t)!==o(r,"palbe"))return null;let a=u(r,t),l=`${n.app}.ts`;if(a!==l&&!a.startsWith(`palbe${d}`))return null;let h=m(),b=o(r,"environments",h,a===l?l:o("palbe",a.slice(`palbe${d}`.length)));if(!y(b))throw new Error(`palbase: ${u(c,b)} is not there \u2014 palbase apps:sync writes it (is ${n.app} set in ${h}, and has ${h} that machine?)`);return b}}}async function j(n,c,p){let i=p?v(n,p):["tsconfig.app.json","tsconfig.json"].map(e=>o(n,e)).find(y);if(!i)return;if(!y(i))throw new Error(`palbase({ tsconfig }): ${u(n,i)} is not there`);let f;try{let e=O(o(n,"package.json")).resolve("typescript/package.json"),s=JSON.parse(S(e,"utf8")).bin?.tsc;if(!s)throw new Error("no tsc");f=o(g(e),s)}catch{throw new Error("palbase: the build type-checks the app against its environment with the app's TypeScript, and the app has none (npm i -D typescript) \u2014 or say palbase({ typecheck: false })")}let m=k(o(W(),"palbase-typecheck-"));try{let e=o(m,"tsconfig.json");P(e,JSON.stringify({extends:i,compilerOptions:{noEmit:!0,composite:!1,incremental:!1,moduleSuffixes:[`.${c}`,""]}}));let s=await new Promise(r=>$(process.execPath,[f,"-p",e,"--pretty","false"],{cwd:n,maxBuffer:16<<20},(t,a,l)=>r({code:t?typeof t.code=="number"?t.code:1:0,text:`${a}${l}`.trim()})));if(s.code!==0)throw new Error(`palbase: the app does not type-check against ${c}, the environment this build is made for \u2014 a route its backend does not have, or a call that does not fit it:
|
|
3
3
|
${s.text}`)}finally{x(m,{recursive:!0,force:!0})}}export{F as palbase};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "palbase",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Palbase for JavaScript: the web client (palbase/web), the server SDK (palbase/server), the backend framework (palbase/backend), AI (palbase/ai) and the CLI (npx palbase).",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://palbase.studio",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"dependencies": {
|
|
136
136
|
"@types/node": "26.6.4",
|
|
137
137
|
"bun-types": "1.4.2",
|
|
138
|
-
"palgres": "0.11.
|
|
138
|
+
"palgres": "0.11.1",
|
|
139
139
|
"reflect-metadata": "^0.2.2",
|
|
140
140
|
"typescript": "~6.0.3",
|
|
141
141
|
"zod": "^4.6.5"
|
|
@@ -4010,7 +4010,7 @@ export type PlanStep = {
|
|
|
4010
4010
|
*/
|
|
4011
4011
|
note?: string;
|
|
4012
4012
|
/**
|
|
4013
|
-
* What it does: create_product, update_product, create_price (Stripe); create_subscription_group, create_subscription, create_in_app_purchase, update_subscription, update_in_app_purchase (what the catalog changed of a product App Store Connect has: its text, its price from tomorrow on
|
|
4013
|
+
* What it does: create_product, update_product, create_price (Stripe); create_subscription_group, create_subscription, create_in_app_purchase, update_subscription, update_in_app_purchase (what the catalog changed of a product App Store Connect has: its text, its price — a subscription's from the day after tomorrow (UTC) on, its subscribers keeping theirs; an in-app purchase's now —, its review screenshot and note), link_subscription, link_in_app_purchase (a product App Store Connect already sells, found by its id, nothing made or changed) (the App Store); create_subscription, create_onetime_product, update_subscription, update_onetime_product, link_subscription, link_onetime_product (Google Play).
|
|
4014
4014
|
*/
|
|
4015
4015
|
op: string;
|
|
4016
4016
|
/**
|