@ait-co/console-cli 0.1.23 → 0.1.24

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/cli.mjs CHANGED
@@ -164,6 +164,48 @@ async function executeAndUnwrap(url, init, fetchImpl) {
164
164
  throw new TossApiError(res.status, parsed.error.errorCode, parsed.error.reason, parsed.error.errorType);
165
165
  }
166
166
  //#endregion
167
+ //#region src/api/certs.ts
168
+ const BASE$5 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
169
+ async function fetchCerts(workspaceId, miniAppId, cookies, opts = {}) {
170
+ const raw = await requestConsoleApi({
171
+ url: `${BASE$5}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs`,
172
+ cookies,
173
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
174
+ });
175
+ if (!Array.isArray(raw)) throw new Error(`Unexpected certs shape for app=${miniAppId}: not an array`);
176
+ return raw.map((c) => {
177
+ if (c === null || typeof c !== "object") return {};
178
+ return c;
179
+ });
180
+ }
181
+ async function issueCert(workspaceId, miniAppId, name, cookies, opts = {}) {
182
+ const raw = await requestConsoleApi({
183
+ url: `${BASE$5}/workspaces/${workspaceId}/mini-app/${miniAppId}/cert/issue`,
184
+ method: "POST",
185
+ body: { name },
186
+ cookies,
187
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
188
+ });
189
+ if (raw === null || typeof raw !== "object") throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: not an object`);
190
+ const rec = raw;
191
+ const privateKey = typeof rec.privateKey === "string" ? rec.privateKey : null;
192
+ const publicKey = typeof rec.publicKey === "string" ? rec.publicKey : null;
193
+ if (privateKey === null || publicKey === null) throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: missing privateKey/publicKey`);
194
+ return {
195
+ privateKey,
196
+ publicKey
197
+ };
198
+ }
199
+ async function revokeCert(workspaceId, miniAppId, certId, cookies, opts = {}) {
200
+ await requestConsoleApi({
201
+ url: `${BASE$5}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs/${encodeURIComponent(certId)}/disable`,
202
+ method: "POST",
203
+ body: {},
204
+ cookies,
205
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
206
+ });
207
+ }
208
+ //#endregion
167
209
  //#region src/api/mini-apps.ts
168
210
  const BASE$4 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
169
211
  async function fetchMiniApps(workspaceId, cookies, opts = {}) {
@@ -329,45 +371,6 @@ async function fetchConversionMetrics(params, cookies, opts = {}) {
329
371
  cacheTime: typeof rec.cacheTime === "string" ? rec.cacheTime : void 0
330
372
  };
331
373
  }
332
- async function fetchCerts(workspaceId, miniAppId, cookies, opts = {}) {
333
- const raw = await requestConsoleApi({
334
- url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs`,
335
- cookies,
336
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
337
- });
338
- if (!Array.isArray(raw)) throw new Error(`Unexpected certs shape for app=${miniAppId}: not an array`);
339
- return raw.map((c) => {
340
- if (c === null || typeof c !== "object") return {};
341
- return c;
342
- });
343
- }
344
- async function issueCert(workspaceId, miniAppId, name, cookies, opts = {}) {
345
- const raw = await requestConsoleApi({
346
- url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/cert/issue`,
347
- method: "POST",
348
- body: { name },
349
- cookies,
350
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
351
- });
352
- if (raw === null || typeof raw !== "object") throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: not an object`);
353
- const rec = raw;
354
- const privateKey = typeof rec.privateKey === "string" ? rec.privateKey : null;
355
- const publicKey = typeof rec.publicKey === "string" ? rec.publicKey : null;
356
- if (privateKey === null || publicKey === null) throw new Error(`Unexpected issue-cert shape for app=${miniAppId}: missing privateKey/publicKey`);
357
- return {
358
- privateKey,
359
- publicKey
360
- };
361
- }
362
- async function revokeCert(workspaceId, miniAppId, certId, cookies, opts = {}) {
363
- await requestConsoleApi({
364
- url: `${BASE$4}/workspaces/${workspaceId}/mini-app/${miniAppId}/certs/${encodeURIComponent(certId)}/disable`,
365
- method: "POST",
366
- body: {},
367
- cookies,
368
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
369
- });
370
- }
371
374
  async function fetchShareRewards(params, cookies, opts = {}) {
372
375
  const qs = new URLSearchParams();
373
376
  qs.set("search", params.search ?? "");
@@ -1587,6 +1590,153 @@ function printContextHeader(ctx, opts) {
1587
1590
  process.stderr.write(line);
1588
1591
  }
1589
1592
  //#endregion
1593
+ //#region src/api/me.ts
1594
+ const BASE$3 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
1595
+ const MEMBER_USER_INFO_URL = `${BASE$3}/members/me/user-info`;
1596
+ async function fetchConsoleMemberUserInfo(cookies, opts = {}) {
1597
+ return requestConsoleApi({
1598
+ url: MEMBER_USER_INFO_URL,
1599
+ cookies,
1600
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1601
+ });
1602
+ }
1603
+ async function fetchUserTerms(cookies, opts = {}) {
1604
+ const raw = await requestConsoleApi({
1605
+ url: `${BASE$3}/console-user-terms/me`,
1606
+ cookies,
1607
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1608
+ });
1609
+ if (!Array.isArray(raw)) throw new Error("Unexpected user-terms shape: not an array");
1610
+ return raw.map((entry, i) => {
1611
+ if (!entry || typeof entry !== "object") throw new Error(`Unexpected user-terms entry at index ${i}`);
1612
+ const e = entry;
1613
+ return {
1614
+ required: Boolean(e.required),
1615
+ termsId: typeof e.termsId === "number" ? e.termsId : 0,
1616
+ revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
1617
+ title: typeof e.title === "string" ? e.title : "",
1618
+ contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
1619
+ actionType: typeof e.actionType === "string" ? e.actionType : "",
1620
+ isAgreed: Boolean(e.isAgreed),
1621
+ isOneTimeConsent: Boolean(e.isOneTimeConsent)
1622
+ };
1623
+ });
1624
+ }
1625
+ //#endregion
1626
+ //#region src/api/workspaces.ts
1627
+ const WORKSPACES_BASE = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
1628
+ async function fetchWorkspaceDetail(workspaceId, cookies, opts = {}) {
1629
+ const raw = await requestConsoleApi({
1630
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}`,
1631
+ cookies,
1632
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1633
+ });
1634
+ const id = raw.id;
1635
+ const name = raw.name;
1636
+ if (typeof id !== "number" || !Number.isInteger(id) || id <= 0 || typeof name !== "string") throw new Error(`Unexpected workspace detail shape for id=${workspaceId}`);
1637
+ const { id: _id, name: _name, ...extra } = raw;
1638
+ return {
1639
+ workspaceId: id,
1640
+ workspaceName: name,
1641
+ extra
1642
+ };
1643
+ }
1644
+ async function fetchWorkspacePartner(workspaceId, cookies, opts = {}) {
1645
+ const raw = await requestConsoleApi({
1646
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/partner`,
1647
+ cookies,
1648
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1649
+ });
1650
+ const registered = raw.registered;
1651
+ if (typeof registered !== "boolean") throw new Error(`Unexpected workspace partner shape for id=${workspaceId}`);
1652
+ return {
1653
+ registered,
1654
+ approvalType: typeof raw.approvalType === "string" ? raw.approvalType : null,
1655
+ rejectMessage: typeof raw.rejectMessage === "string" ? raw.rejectMessage : null,
1656
+ partner: raw.partner && typeof raw.partner === "object" ? raw.partner : null
1657
+ };
1658
+ }
1659
+ const WORKSPACE_TERM_TYPES = [
1660
+ "TOSS_LOGIN",
1661
+ "BIZ_WORKSPACE",
1662
+ "TOSS_PROMOTION_MONEY",
1663
+ "IAA",
1664
+ "IAP"
1665
+ ];
1666
+ const DEFAULT_SEGMENT_CATEGORY = "생성된 세그먼트";
1667
+ async function fetchWorkspaceSegments(params, cookies, opts = {}) {
1668
+ const page = params.page ?? 0;
1669
+ const qs = new URLSearchParams();
1670
+ qs.set("category", params.category ?? DEFAULT_SEGMENT_CATEGORY);
1671
+ qs.set("search", params.search ?? "");
1672
+ qs.set("page", String(page));
1673
+ const raw = await requestConsoleApi({
1674
+ url: `${WORKSPACES_BASE}/workspaces/${params.workspaceId}/segments/list?${qs.toString()}`,
1675
+ cookies,
1676
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1677
+ });
1678
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Unexpected segments shape for workspace=${params.workspaceId}`);
1679
+ const data = raw;
1680
+ return {
1681
+ contents: (Array.isArray(data.contents) ? data.contents : []).map((c) => c && typeof c === "object" ? c : {}),
1682
+ totalPage: typeof data.totalPage === "number" ? data.totalPage : 0,
1683
+ currentPage: typeof data.currentPage === "number" ? data.currentPage : page
1684
+ };
1685
+ }
1686
+ async function fetchWorkspaceTerms(workspaceId, type, cookies, opts = {}) {
1687
+ const raw = await requestConsoleApi({
1688
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms/${type}/skip-permission`,
1689
+ cookies,
1690
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1691
+ });
1692
+ if (!Array.isArray(raw)) throw new Error(`Unexpected workspace terms shape for type=${type}`);
1693
+ return raw.map((entry, i) => {
1694
+ if (!entry || typeof entry !== "object") throw new Error(`Unexpected workspace terms entry at index ${i} for type=${type}`);
1695
+ const e = entry;
1696
+ return {
1697
+ required: Boolean(e.required),
1698
+ termsId: typeof e.termsId === "number" ? e.termsId : 0,
1699
+ revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
1700
+ title: typeof e.title === "string" ? e.title : "",
1701
+ contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
1702
+ actionType: typeof e.actionType === "string" ? e.actionType : "",
1703
+ isAgreed: Boolean(e.isAgreed),
1704
+ isOneTimeConsent: Boolean(e.isOneTimeConsent)
1705
+ };
1706
+ });
1707
+ }
1708
+ /**
1709
+ * Persist agreement for one-or-more workspace terms. The endpoint takes a
1710
+ * single `agreedList` regardless of which bucket the terms came from — the
1711
+ * type tag is implicit in the (termsId, revisionId) pairs.
1712
+ *
1713
+ * Captured behaviour (2026-05-08, ws=36577):
1714
+ * - `POST /workspaces/<wid>/console-workspace-terms` with body
1715
+ * `{"agreedList":[{"termsId": <int>, "revisionId": <int>}, ...]}`
1716
+ * - Response on success: `{"resultType":"SUCCESS","success":{}}` — no
1717
+ * useful payload. We resolve `void`.
1718
+ * - Re-submitting an already-agreed term returns `errorCode: 500`
1719
+ * (Internal Server Error). The server is NOT idempotent, so callers
1720
+ * must filter to `isAgreed === false` before invoking.
1721
+ * - Empty `agreedList` returns SUCCESS (no-op), but we throw client-side
1722
+ * before sending — round-tripping a no-op request is wasted.
1723
+ *
1724
+ * Failure surfaces through `TossApiError` like every other write helper.
1725
+ */
1726
+ async function agreeWorkspaceTerms(workspaceId, terms, cookies, opts = {}) {
1727
+ if (terms.length === 0) throw new Error("agreeWorkspaceTerms requires at least one term");
1728
+ await requestConsoleApi({
1729
+ method: "POST",
1730
+ url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms`,
1731
+ cookies,
1732
+ body: { agreedList: terms.map(({ termsId, revisionId }) => ({
1733
+ termsId,
1734
+ revisionId
1735
+ })) },
1736
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
1737
+ });
1738
+ }
1739
+ //#endregion
1590
1740
  //#region src/config/ait-bundle.ts
1591
1741
  var AitBundleError = class extends Error {
1592
1742
  path;
@@ -1878,30 +2028,21 @@ async function runDeploy(args, deps = {}) {
1878
2028
  const steps = ["upload"];
1879
2029
  if (requestReview) steps.push("review");
1880
2030
  if (release) steps.push("release");
1881
- if (args.dryRun) {
1882
- if (args.json) emitJson({
1883
- ok: true,
1884
- dryRun: true,
1885
- workspaceId,
1886
- appId,
1887
- deploymentId,
1888
- bundleFormat: bundleInfo.format,
1889
- bytes: bundleInfo.bytes.byteLength,
1890
- steps,
1891
- memo: memo ?? null,
1892
- releaseNotes: releaseNotes ?? null,
1893
- confirmed: confirm
1894
- });
1895
- else {
1896
- const stepsLine = steps.map((s) => {
1897
- if (s === "review") return `review (releaseNotes: ${JSON.stringify(releaseNotes ?? "")})`;
1898
- if (s === "release") return `release (${confirm ? "confirmed" : "NOT confirmed"})`;
1899
- return s;
1900
- }).join(" → ");
1901
- process.stdout.write(`DRY RUN\n app ${appId}\n workspace ${workspaceId}\n bundle ${args.path} (${bundleInfo.bytes.byteLength} bytes)\n deploymentId ${deploymentId}\n memo ${memo ?? "(none)"}\n steps ${stepsLine}\n`);
1902
- }
1903
- return exitAfterFlush(ExitCode.Ok);
1904
- }
2031
+ if (args.dryRun) return runDryRun({
2032
+ json: args.json,
2033
+ path: args.path,
2034
+ bundleInfo,
2035
+ deploymentId,
2036
+ explicitDeploymentId: typeof args.deploymentId === "string" && args.deploymentId !== "",
2037
+ workspaceId,
2038
+ appId,
2039
+ session,
2040
+ steps,
2041
+ memo,
2042
+ releaseNotes,
2043
+ confirm,
2044
+ fetchImpl: deps.fetchImpl
2045
+ });
1905
2046
  const apiOpts = deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {};
1906
2047
  let uploaded = false;
1907
2048
  let bundleRecord = null;
@@ -2037,38 +2178,154 @@ async function emitPartialFailure(json, err, progress) {
2037
2178
  else process.stderr.write(`Unexpected error: ${err.message}\n`);
2038
2179
  return exitAfterFlush(ExitCode.ApiError);
2039
2180
  }
2040
- //#endregion
2041
- //#region src/api/me.ts
2042
- const BASE$3 = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
2043
- const MEMBER_USER_INFO_URL = `${BASE$3}/members/me/user-info`;
2044
- async function fetchConsoleMemberUserInfo(cookies, opts = {}) {
2045
- return requestConsoleApi({
2046
- url: MEMBER_USER_INFO_URL,
2047
- cookies,
2048
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
2049
- });
2181
+ const WORKSPACE_TERM_ERROR_CODES = {
2182
+ TOSS_LOGIN: 4037,
2183
+ BIZ_WORKSPACE: 4040,
2184
+ TOSS_PROMOTION_MONEY: 4039,
2185
+ IAA: 4099,
2186
+ IAP: 5001
2187
+ };
2188
+ async function runDryRun(input) {
2189
+ const apiOpts = input.fetchImpl ? { fetchImpl: input.fetchImpl } : {};
2190
+ const embedded = input.bundleInfo.deploymentId;
2191
+ const flagMatch = input.explicitDeploymentId ? input.deploymentId === embedded : null;
2192
+ const [permissions, terms] = await Promise.all([fetchPermissions(input.workspaceId, input.session, apiOpts), fetchTermsBlockers(input.workspaceId, input.session, apiOpts)]);
2193
+ const wouldSucceed = (flagMatch === null || flagMatch === true) && terms.blockers.length === 0;
2194
+ if (input.json) {
2195
+ emitJson({
2196
+ ok: true,
2197
+ dryRun: true,
2198
+ wouldSucceed,
2199
+ workspaceId: input.workspaceId,
2200
+ appId: input.appId,
2201
+ deploymentId: input.deploymentId,
2202
+ bundleFormat: input.bundleInfo.format,
2203
+ bytes: input.bundleInfo.bytes.byteLength,
2204
+ steps: input.steps,
2205
+ memo: input.memo ?? null,
2206
+ releaseNotes: input.releaseNotes ?? null,
2207
+ confirmed: input.confirm,
2208
+ bundle: {
2209
+ path: input.path,
2210
+ format: input.bundleInfo.format,
2211
+ deploymentId: input.deploymentId,
2212
+ embeddedDeploymentId: embedded,
2213
+ deploymentIdSource: input.explicitDeploymentId ? "flag" : "bundle",
2214
+ flagMatch,
2215
+ size: input.bundleInfo.bytes.byteLength
2216
+ },
2217
+ context: {
2218
+ workspaceId: input.workspaceId,
2219
+ appId: input.appId,
2220
+ sessionValid: true,
2221
+ permissions
2222
+ },
2223
+ terms
2224
+ });
2225
+ return exitAfterFlush(ExitCode.Ok);
2226
+ }
2227
+ process.stdout.write(renderDryRunText(input, {
2228
+ embedded,
2229
+ flagMatch,
2230
+ permissions,
2231
+ terms,
2232
+ wouldSucceed
2233
+ }));
2234
+ return exitAfterFlush(ExitCode.Ok);
2050
2235
  }
2051
- async function fetchUserTerms(cookies, opts = {}) {
2052
- const raw = await requestConsoleApi({
2053
- url: `${BASE$3}/console-user-terms/me`,
2054
- cookies,
2055
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
2056
- });
2057
- if (!Array.isArray(raw)) throw new Error("Unexpected user-terms shape: not an array");
2058
- return raw.map((entry, i) => {
2059
- if (!entry || typeof entry !== "object") throw new Error(`Unexpected user-terms entry at index ${i}`);
2060
- const e = entry;
2236
+ async function fetchPermissions(workspaceId, session, apiOpts) {
2237
+ try {
2238
+ const ws = (await fetchConsoleMemberUserInfo(session.cookies, apiOpts)).workspaces.find((w) => w.workspaceId === workspaceId);
2239
+ if (!ws) return {
2240
+ role: null,
2241
+ source: "unknown",
2242
+ error: `current user has no membership in workspace ${workspaceId}`
2243
+ };
2061
2244
  return {
2062
- required: Boolean(e.required),
2063
- termsId: typeof e.termsId === "number" ? e.termsId : 0,
2064
- revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
2065
- title: typeof e.title === "string" ? e.title : "",
2066
- contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
2067
- actionType: typeof e.actionType === "string" ? e.actionType : "",
2068
- isAgreed: Boolean(e.isAgreed),
2069
- isOneTimeConsent: Boolean(e.isOneTimeConsent)
2245
+ role: ws.role,
2246
+ source: "members/me"
2070
2247
  };
2071
- });
2248
+ } catch (err) {
2249
+ return {
2250
+ role: null,
2251
+ source: "unknown",
2252
+ error: err.message
2253
+ };
2254
+ }
2255
+ }
2256
+ async function fetchTermsBlockers(workspaceId, session, apiOpts) {
2257
+ try {
2258
+ const [userTerms, workspaceResults] = await Promise.all([fetchUserTerms(session.cookies, apiOpts), Promise.all(WORKSPACE_TERM_TYPES.map(async (t) => [t, await fetchWorkspaceTerms(workspaceId, t, session.cookies, apiOpts)]))]);
2259
+ const blockers = [];
2260
+ for (const t of userTerms) if (t.required && !t.isAgreed) blockers.push({
2261
+ scope: "user",
2262
+ type: "USER_TERMS",
2263
+ errorCode: 4036,
2264
+ title: t.title,
2265
+ action: "aitcc me terms"
2266
+ });
2267
+ for (const [type, terms] of workspaceResults) for (const t of terms) {
2268
+ if (!t.required || t.isAgreed) continue;
2269
+ blockers.push({
2270
+ scope: "workspace",
2271
+ type,
2272
+ errorCode: WORKSPACE_TERM_ERROR_CODES[type],
2273
+ title: t.title,
2274
+ action: `aitcc workspace terms --type ${type}`
2275
+ });
2276
+ }
2277
+ return {
2278
+ blockers,
2279
+ checked: true
2280
+ };
2281
+ } catch (err) {
2282
+ return {
2283
+ blockers: [],
2284
+ checked: false,
2285
+ error: err.message
2286
+ };
2287
+ }
2288
+ }
2289
+ function renderDryRunText(input, derived) {
2290
+ const lines = [];
2291
+ lines.push(`DRY RUN — app deploy ${input.appId}\n`);
2292
+ lines.push("\nBundle\n");
2293
+ lines.push(` path ${input.path}\n`);
2294
+ lines.push(` format ${input.bundleInfo.format.toUpperCase()}\n`);
2295
+ lines.push(` deploymentId ${input.deploymentId}\n`);
2296
+ if (derived.flagMatch === false) lines.push(` flag match MISMATCH (bundle embeds ${derived.embedded})\n`);
2297
+ else if (derived.flagMatch === true) lines.push(` flag match ok (matches embedded)\n`);
2298
+ lines.push(` size ${formatBytes(input.bundleInfo.bytes.byteLength)}\n`);
2299
+ lines.push("\nContext\n");
2300
+ lines.push(` workspace ${input.workspaceId}\n`);
2301
+ lines.push(` app ${input.appId}\n`);
2302
+ lines.push(` session valid\n`);
2303
+ if (derived.permissions.role !== null) lines.push(` permissions ${derived.permissions.role}\n`);
2304
+ else lines.push(` permissions unknown${derived.permissions.error ? ` (${derived.permissions.error})` : ""}\n`);
2305
+ lines.push("\nTerms\n");
2306
+ if (!derived.terms.checked) {
2307
+ lines.push(` warning: could not check terms status (${derived.terms.error ?? "unknown error"}).\n`);
2308
+ lines.push(" live deploy may still fail with a 4032/4036/4037/4039/4040/4099/5001 errorCode.\n");
2309
+ } else if (derived.terms.blockers.length === 0) lines.push(" all deploy-related terms are agreed\n");
2310
+ else for (const b of derived.terms.blockers) lines.push(` blocked: ${b.scope}/${b.type} — ${b.title} (errorCode ${b.errorCode})\n action: ${b.action}\n`);
2311
+ lines.push("\nPlan\n");
2312
+ const stepsLine = input.steps.map((s) => {
2313
+ if (s === "review") return `review (releaseNotes: ${JSON.stringify(input.releaseNotes ?? "")})`;
2314
+ if (s === "release") return `release (${input.confirm ? "confirmed" : "NOT confirmed"})`;
2315
+ return s;
2316
+ }).join(" → ");
2317
+ lines.push(` steps ${stepsLine}\n`);
2318
+ lines.push(` memo ${input.memo ?? "(none)"}\n`);
2319
+ lines.push("\nResult\n");
2320
+ if (!derived.wouldSucceed) lines.push(" Live deploy would fail. Resolve the blocked items above, then re-run.\n");
2321
+ else if (derived.permissions.role === null) lines.push(" Live deploy would clear bundle + terms checks. Workspace membership could not be confirmed; live deploy may still fail with a permissions error.\n");
2322
+ else lines.push(" Live deploy would clear every pre-flight check.\n");
2323
+ return lines.join("");
2324
+ }
2325
+ function formatBytes(n) {
2326
+ if (n < 1024) return `${n} B`;
2327
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
2328
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
2072
2329
  }
2073
2330
  //#endregion
2074
2331
  //#region src/config/app-manifest.ts
@@ -4296,6 +4553,25 @@ const bundlesCommand = defineCommand({
4296
4553
  })
4297
4554
  }
4298
4555
  });
4556
+ const CERT_EXPIRY_WARN_DAYS = 30;
4557
+ const MS_PER_DAY = 864e5;
4558
+ function deriveDaysUntilExpiry(cert, now) {
4559
+ const raw = cert.expireTs;
4560
+ let ts = null;
4561
+ if (typeof raw === "number" && Number.isFinite(raw)) ts = raw;
4562
+ else if (typeof raw === "string") {
4563
+ const parsed = Date.parse(raw);
4564
+ if (Number.isFinite(parsed)) ts = parsed;
4565
+ }
4566
+ if (ts === null) return null;
4567
+ return Math.floor((ts - now) / MS_PER_DAY);
4568
+ }
4569
+ function expiryMarker(days) {
4570
+ if (days === null) return "";
4571
+ if (days < 0) return `\t⚠ 만료됨`;
4572
+ if (days <= CERT_EXPIRY_WARN_DAYS) return `\t⚠ 만료 임박 (${days}일)`;
4573
+ return "";
4574
+ }
4299
4575
  const certsLsCommand = defineCommand({
4300
4576
  meta: {
4301
4577
  name: "ls",
@@ -4330,27 +4606,153 @@ const certsLsCommand = defineCommand({
4330
4606
  const { session, workspaceId } = ctx;
4331
4607
  try {
4332
4608
  const certs = await fetchCerts(workspaceId, appId, session.cookies);
4609
+ const now = Date.now();
4610
+ const augmented = certs.map((c) => ({
4611
+ ...c,
4612
+ daysUntilExpiry: deriveDaysUntilExpiry(c, now)
4613
+ }));
4333
4614
  if (args.json) {
4334
4615
  emitJson({
4335
4616
  ok: true,
4336
4617
  workspaceId,
4337
4618
  appId,
4338
- certs
4619
+ certs: augmented
4339
4620
  });
4340
4621
  return exitAfterFlush(ExitCode.Ok);
4341
4622
  }
4342
- if (certs.length === 0) {
4623
+ if (augmented.length === 0) {
4343
4624
  process.stdout.write(`App ${appId} (ws ${workspaceId}): no mTLS certs\n`);
4344
4625
  return exitAfterFlush(ExitCode.Ok);
4345
4626
  }
4346
- process.stdout.write(`App ${appId} (ws ${workspaceId}): ${certs.length} cert(s)\n`);
4347
- for (const c of certs) {
4627
+ process.stdout.write(`App ${appId} (ws ${workspaceId}): ${augmented.length} cert(s)\n`);
4628
+ for (const c of augmented) {
4348
4629
  const id = typeof c.id === "string" || typeof c.id === "number" ? c.id : typeof c.certId === "string" || typeof c.certId === "number" ? c.certId : "-";
4349
4630
  const cn = typeof c.commonName === "string" ? c.commonName : "-";
4350
4631
  const createdAt = typeof c.createdAt === "string" ? c.createdAt : "";
4351
- const expiresAt = typeof c.expiresAt === "string" ? c.expiresAt : typeof c.validUntil === "string" ? c.validUntil : "";
4352
- process.stdout.write(`${id}\t${cn}\t${createdAt}\t${expiresAt}\n`);
4632
+ const expiresAt = typeof c.expiresAt === "string" ? c.expiresAt : typeof c.validUntil === "string" ? c.validUntil : typeof c.expireTs === "number" && Number.isFinite(c.expireTs) ? new Date(c.expireTs).toISOString() : "";
4633
+ process.stdout.write(`${id}\t${cn}\t${createdAt}\t${expiresAt}${expiryMarker(c.daysUntilExpiry)}\n`);
4634
+ }
4635
+ return exitAfterFlush(ExitCode.Ok);
4636
+ } catch (err) {
4637
+ return emitFailureFromError(args.json, err);
4638
+ }
4639
+ }
4640
+ });
4641
+ function pickCertById(certs, certId) {
4642
+ const target = certId.trim();
4643
+ if (target.length === 0) return null;
4644
+ for (const c of certs) {
4645
+ const candidate = typeof c.id === "string" || typeof c.id === "number" ? c.id : typeof c.certId === "string" || typeof c.certId === "number" ? c.certId : null;
4646
+ if (candidate !== null && String(candidate) === target) return c;
4647
+ }
4648
+ return null;
4649
+ }
4650
+ function augmentCertExpiry(cert, now = Date.now()) {
4651
+ let expiresAtMs;
4652
+ if (typeof cert.expireTs === "number" && Number.isFinite(cert.expireTs)) expiresAtMs = cert.expireTs;
4653
+ else if (typeof cert.expiresAt === "string") {
4654
+ const t = Date.parse(cert.expiresAt);
4655
+ if (Number.isFinite(t)) expiresAtMs = t;
4656
+ } else if (typeof cert.validUntil === "string") {
4657
+ const t = Date.parse(cert.validUntil);
4658
+ if (Number.isFinite(t)) expiresAtMs = t;
4659
+ }
4660
+ if (expiresAtMs === void 0) return {};
4661
+ const daysUntilExpiry = Math.floor((expiresAtMs - now) / 864e5);
4662
+ return {
4663
+ expiresAtMs,
4664
+ daysUntilExpiry
4665
+ };
4666
+ }
4667
+ const certsShowCommand = defineCommand({
4668
+ meta: {
4669
+ name: "show",
4670
+ description: "Show a single mTLS certificate by id (metadata only — no PEM)."
4671
+ },
4672
+ args: {
4673
+ certId: {
4674
+ type: "positional",
4675
+ description: "Cert ID (from `app certs ls`).",
4676
+ required: true
4677
+ },
4678
+ app: {
4679
+ type: "string",
4680
+ description: "Mini-app ID the cert belongs to."
4681
+ },
4682
+ workspace: {
4683
+ type: "string",
4684
+ description: "Workspace ID. Defaults to the selected workspace."
4685
+ },
4686
+ json: {
4687
+ type: "boolean",
4688
+ description: "Emit machine-readable JSON.",
4689
+ default: false
4690
+ }
4691
+ },
4692
+ async run({ args }) {
4693
+ const certId = typeof args.certId === "string" ? args.certId.trim() : "";
4694
+ if (certId.length === 0) {
4695
+ if (args.json) emitJson({
4696
+ ok: false,
4697
+ reason: "missing-cert-id",
4698
+ message: "certId positional is required"
4699
+ });
4700
+ else process.stderr.write("app certs show: certId is required\n");
4701
+ return exitAfterFlush(ExitCode.Usage);
4702
+ }
4703
+ const ctx = await resolveAppOrFail({
4704
+ json: args.json,
4705
+ appIdRaw: args.app,
4706
+ appIdField: "app",
4707
+ ...args.workspace !== void 0 ? { workspace: args.workspace } : {}
4708
+ });
4709
+ if (!ctx) return;
4710
+ const appId = await requireMiniAppId(ctx, args.json);
4711
+ if (appId === null) return;
4712
+ printContextHeader(ctx, { json: args.json });
4713
+ const { session, workspaceId } = ctx;
4714
+ try {
4715
+ const match = pickCertById(await fetchCerts(workspaceId, appId, session.cookies), certId);
4716
+ if (match === null) {
4717
+ if (args.json) emitJson({
4718
+ ok: false,
4719
+ reason: "not-found",
4720
+ workspaceId,
4721
+ appId,
4722
+ certId,
4723
+ message: `cert ${certId} not found on app ${appId}`
4724
+ });
4725
+ else process.stderr.write(`app certs show: cert ${certId} not found on app ${appId} (ws ${workspaceId})\n`);
4726
+ return exitAfterFlush(ExitCode.Usage);
4727
+ }
4728
+ const expiry = augmentCertExpiry(match);
4729
+ if (args.json) {
4730
+ emitJson({
4731
+ ok: true,
4732
+ workspaceId,
4733
+ appId,
4734
+ cert: match,
4735
+ ...expiry
4736
+ });
4737
+ return exitAfterFlush(ExitCode.Ok);
4738
+ }
4739
+ const id = typeof match.id === "string" || typeof match.id === "number" ? match.id : typeof match.certId === "string" || typeof match.certId === "number" ? match.certId : "-";
4740
+ const name = typeof match.name === "string" ? match.name : "-";
4741
+ const cn = typeof match.commonName === "string" ? match.commonName : "";
4742
+ const createdAt = typeof match.createdAt === "string" ? match.createdAt : "";
4743
+ const expiresAtIso = expiry.expiresAtMs !== void 0 ? new Date(expiry.expiresAtMs).toISOString() : "";
4744
+ const status = typeof match.status === "string" ? match.status : "";
4745
+ const lines = [`App ${appId} (ws ${workspaceId}): cert ${id}`, ` name: ${name}`];
4746
+ if (cn) lines.push(` commonName: ${cn}`);
4747
+ if (createdAt) lines.push(` createdAt: ${createdAt}`);
4748
+ if (expiresAtIso) lines.push(` expiresAt: ${expiresAtIso}`);
4749
+ if (expiry.daysUntilExpiry !== void 0) {
4750
+ const d = expiry.daysUntilExpiry;
4751
+ const suffix = d > 0 ? ` (D-${d})` : d === 0 ? " (expires today)" : ` (expired ${-d} day(s) ago)`;
4752
+ lines.push(` daysUntilExpiry: ${d}${suffix}`);
4353
4753
  }
4754
+ if (status) lines.push(` status: ${status}`);
4755
+ process.stdout.write(`${lines.join("\n")}\n`);
4354
4756
  return exitAfterFlush(ExitCode.Ok);
4355
4757
  } catch (err) {
4356
4758
  return emitFailureFromError(args.json, err);
@@ -4370,6 +4772,7 @@ const certsCommand = defineCommand({
4370
4772
  },
4371
4773
  subCommands: {
4372
4774
  ls: certsLsCommand,
4775
+ show: certsShowCommand,
4373
4776
  issue: defineCommand({
4374
4777
  meta: {
4375
4778
  name: "issue",
@@ -8498,7 +8901,7 @@ function resolveVersion() {
8498
8901
  if (typeof injected === "string" && injected.length > 0) return injected;
8499
8902
  } catch {}
8500
8903
  try {
8501
- return "0.1.23";
8904
+ return "0.1.24";
8502
8905
  } catch {}
8503
8906
  return "0.0.0-dev";
8504
8907
  }
@@ -8986,120 +9389,6 @@ const whoamiCommand = defineCommand({
8986
9389
  }
8987
9390
  });
8988
9391
  //#endregion
8989
- //#region src/api/workspaces.ts
8990
- const WORKSPACES_BASE = "https://apps-in-toss.toss.im/console/api-public/v3/appsintossconsole";
8991
- async function fetchWorkspaceDetail(workspaceId, cookies, opts = {}) {
8992
- const raw = await requestConsoleApi({
8993
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}`,
8994
- cookies,
8995
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
8996
- });
8997
- const id = raw.id;
8998
- const name = raw.name;
8999
- if (typeof id !== "number" || !Number.isInteger(id) || id <= 0 || typeof name !== "string") throw new Error(`Unexpected workspace detail shape for id=${workspaceId}`);
9000
- const { id: _id, name: _name, ...extra } = raw;
9001
- return {
9002
- workspaceId: id,
9003
- workspaceName: name,
9004
- extra
9005
- };
9006
- }
9007
- async function fetchWorkspacePartner(workspaceId, cookies, opts = {}) {
9008
- const raw = await requestConsoleApi({
9009
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/partner`,
9010
- cookies,
9011
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9012
- });
9013
- const registered = raw.registered;
9014
- if (typeof registered !== "boolean") throw new Error(`Unexpected workspace partner shape for id=${workspaceId}`);
9015
- return {
9016
- registered,
9017
- approvalType: typeof raw.approvalType === "string" ? raw.approvalType : null,
9018
- rejectMessage: typeof raw.rejectMessage === "string" ? raw.rejectMessage : null,
9019
- partner: raw.partner && typeof raw.partner === "object" ? raw.partner : null
9020
- };
9021
- }
9022
- const WORKSPACE_TERM_TYPES = [
9023
- "TOSS_LOGIN",
9024
- "BIZ_WORKSPACE",
9025
- "TOSS_PROMOTION_MONEY",
9026
- "IAA",
9027
- "IAP"
9028
- ];
9029
- const DEFAULT_SEGMENT_CATEGORY = "생성된 세그먼트";
9030
- async function fetchWorkspaceSegments(params, cookies, opts = {}) {
9031
- const page = params.page ?? 0;
9032
- const qs = new URLSearchParams();
9033
- qs.set("category", params.category ?? DEFAULT_SEGMENT_CATEGORY);
9034
- qs.set("search", params.search ?? "");
9035
- qs.set("page", String(page));
9036
- const raw = await requestConsoleApi({
9037
- url: `${WORKSPACES_BASE}/workspaces/${params.workspaceId}/segments/list?${qs.toString()}`,
9038
- cookies,
9039
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9040
- });
9041
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Unexpected segments shape for workspace=${params.workspaceId}`);
9042
- const data = raw;
9043
- return {
9044
- contents: (Array.isArray(data.contents) ? data.contents : []).map((c) => c && typeof c === "object" ? c : {}),
9045
- totalPage: typeof data.totalPage === "number" ? data.totalPage : 0,
9046
- currentPage: typeof data.currentPage === "number" ? data.currentPage : page
9047
- };
9048
- }
9049
- async function fetchWorkspaceTerms(workspaceId, type, cookies, opts = {}) {
9050
- const raw = await requestConsoleApi({
9051
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms/${type}/skip-permission`,
9052
- cookies,
9053
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9054
- });
9055
- if (!Array.isArray(raw)) throw new Error(`Unexpected workspace terms shape for type=${type}`);
9056
- return raw.map((entry, i) => {
9057
- if (!entry || typeof entry !== "object") throw new Error(`Unexpected workspace terms entry at index ${i} for type=${type}`);
9058
- const e = entry;
9059
- return {
9060
- required: Boolean(e.required),
9061
- termsId: typeof e.termsId === "number" ? e.termsId : 0,
9062
- revisionId: typeof e.revisionId === "number" ? e.revisionId : 0,
9063
- title: typeof e.title === "string" ? e.title : "",
9064
- contentsUrl: typeof e.contentsUrl === "string" ? e.contentsUrl : "",
9065
- actionType: typeof e.actionType === "string" ? e.actionType : "",
9066
- isAgreed: Boolean(e.isAgreed),
9067
- isOneTimeConsent: Boolean(e.isOneTimeConsent)
9068
- };
9069
- });
9070
- }
9071
- /**
9072
- * Persist agreement for one-or-more workspace terms. The endpoint takes a
9073
- * single `agreedList` regardless of which bucket the terms came from — the
9074
- * type tag is implicit in the (termsId, revisionId) pairs.
9075
- *
9076
- * Captured behaviour (2026-05-08, ws=36577):
9077
- * - `POST /workspaces/<wid>/console-workspace-terms` with body
9078
- * `{"agreedList":[{"termsId": <int>, "revisionId": <int>}, ...]}`
9079
- * - Response on success: `{"resultType":"SUCCESS","success":{}}` — no
9080
- * useful payload. We resolve `void`.
9081
- * - Re-submitting an already-agreed term returns `errorCode: 500`
9082
- * (Internal Server Error). The server is NOT idempotent, so callers
9083
- * must filter to `isAgreed === false` before invoking.
9084
- * - Empty `agreedList` returns SUCCESS (no-op), but we throw client-side
9085
- * before sending — round-tripping a no-op request is wasted.
9086
- *
9087
- * Failure surfaces through `TossApiError` like every other write helper.
9088
- */
9089
- async function agreeWorkspaceTerms(workspaceId, terms, cookies, opts = {}) {
9090
- if (terms.length === 0) throw new Error("agreeWorkspaceTerms requires at least one term");
9091
- await requestConsoleApi({
9092
- method: "POST",
9093
- url: `${WORKSPACES_BASE}/workspaces/${workspaceId}/console-workspace-terms`,
9094
- cookies,
9095
- body: { agreedList: terms.map(({ termsId, revisionId }) => ({
9096
- termsId,
9097
- revisionId
9098
- })) },
9099
- ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
9100
- });
9101
- }
9102
- //#endregion
9103
9392
  //#region src/commands/workspace.ts
9104
9393
  function formatScalar(v) {
9105
9394
  if (v === null) return "null";