From 66e3862ed7e3cee0e8e98a2ad487b405808356d1 Mon Sep 17 00:00:00 2001 From: vigneshrajsb Date: Tue, 15 Sep 2026 13:31:04 -0700 Subject: [PATCH 1/6] feat(sites): add visibility commands and owner access --- README.md | 9 +- src/commands/sites.ts | 181 +++++++++++------- src/lib/api.ts | 40 ++-- src/lib/generated/bindSitesBrowserLogin200.ts | 12 ++ .../generated/bindSitesBrowserLogin200Data.ts | 11 ++ src/lib/generated/deleteSiteParams.ts | 11 ++ src/lib/generated/extendSiteParams.ts | 11 ++ src/lib/generated/index.ts | 33 +++- src/lib/generated/listSitesParams.ts | 7 +- src/lib/generated/listSitesView.ts | 17 ++ src/lib/generated/mintSitesBrowserLogin200.ts | 12 ++ .../generated/mintSitesBrowserLogin200Data.ts | 12 ++ .../resolvePublicSiteBrowserUrl200.ts | 12 ++ .../resolvePublicSiteBrowserUrl200Data.ts | 11 ++ .../resolvePublicSiteBrowserUrl404.ts | 12 ++ .../resolvePublicSiteBrowserUrl404Error.ts | 12 ++ ...olvePublicSiteBrowserUrl404ErrorMessage.ts | 15 ++ .../generated/revokeSitesBrowserLogin200.ts | 12 ++ .../revokeSitesBrowserLogin200Data.ts | 11 ++ src/lib/generated/setSiteVisibilityBody.ts | 17 ++ .../setSiteVisibilityBodyVisibility.ts | 16 ++ src/lib/generated/site.ts | 16 ++ src/lib/generated/siteCurrentRole.ts | 18 ++ src/lib/generated/sitePermissions.ts | 14 ++ src/lib/generated/siteUploadRequest.ts | 13 ++ .../generated/siteUploadRequestVisibility.ts | 19 ++ src/lib/generated/siteVisibility.ts | 16 ++ src/lib/generated/sitesBrowserBindRequest.ts | 11 ++ .../generated/sitesBrowserBindRequestAllOf.ts | 12 ++ src/lib/generated/sitesBrowserLoginRequest.ts | 17 ++ src/lib/generated/sitesBrowserMintRequest.ts | 11 ++ .../generated/sitesBrowserMintRequestAllOf.ts | 17 ++ src/lib/generated/sitesCapabilities.ts | 18 ++ ...itesCapabilitiesAllowedVisibilitiesItem.ts | 16 ++ .../sitesCapabilitiesDefaultVisibility.ts | 16 ++ .../sitesCapabilitiesSuccessResponse.ts | 11 ++ .../sitesCapabilitiesSuccessResponseAllOf.ts | 12 ++ src/lib/generated/sitesCapabilitiesUpload.ts | 17 ++ src/lib/sites.ts | 11 ++ src/lib/types.ts | 8 +- tests/sites-command.test.ts | 47 +++++ tests/sites.test.ts | 78 ++++++++ 42 files changed, 773 insertions(+), 99 deletions(-) create mode 100644 src/lib/generated/bindSitesBrowserLogin200.ts create mode 100644 src/lib/generated/bindSitesBrowserLogin200Data.ts create mode 100644 src/lib/generated/deleteSiteParams.ts create mode 100644 src/lib/generated/extendSiteParams.ts create mode 100644 src/lib/generated/listSitesView.ts create mode 100644 src/lib/generated/mintSitesBrowserLogin200.ts create mode 100644 src/lib/generated/mintSitesBrowserLogin200Data.ts create mode 100644 src/lib/generated/resolvePublicSiteBrowserUrl200.ts create mode 100644 src/lib/generated/resolvePublicSiteBrowserUrl200Data.ts create mode 100644 src/lib/generated/resolvePublicSiteBrowserUrl404.ts create mode 100644 src/lib/generated/resolvePublicSiteBrowserUrl404Error.ts create mode 100644 src/lib/generated/resolvePublicSiteBrowserUrl404ErrorMessage.ts create mode 100644 src/lib/generated/revokeSitesBrowserLogin200.ts create mode 100644 src/lib/generated/revokeSitesBrowserLogin200Data.ts create mode 100644 src/lib/generated/setSiteVisibilityBody.ts create mode 100644 src/lib/generated/setSiteVisibilityBodyVisibility.ts create mode 100644 src/lib/generated/siteCurrentRole.ts create mode 100644 src/lib/generated/sitePermissions.ts create mode 100644 src/lib/generated/siteUploadRequestVisibility.ts create mode 100644 src/lib/generated/siteVisibility.ts create mode 100644 src/lib/generated/sitesBrowserBindRequest.ts create mode 100644 src/lib/generated/sitesBrowserBindRequestAllOf.ts create mode 100644 src/lib/generated/sitesBrowserLoginRequest.ts create mode 100644 src/lib/generated/sitesBrowserMintRequest.ts create mode 100644 src/lib/generated/sitesBrowserMintRequestAllOf.ts create mode 100644 src/lib/generated/sitesCapabilities.ts create mode 100644 src/lib/generated/sitesCapabilitiesAllowedVisibilitiesItem.ts create mode 100644 src/lib/generated/sitesCapabilitiesDefaultVisibility.ts create mode 100644 src/lib/generated/sitesCapabilitiesSuccessResponse.ts create mode 100644 src/lib/generated/sitesCapabilitiesSuccessResponseAllOf.ts create mode 100644 src/lib/generated/sitesCapabilitiesUpload.ts create mode 100644 src/lib/sites.ts create mode 100644 tests/sites-command.test.ts create mode 100644 tests/sites.test.ts diff --git a/README.md b/README.md index b880e4e..e1a836a 100644 --- a/README.md +++ b/README.md @@ -159,15 +159,22 @@ Runs fully offline (no auth needed) except `--repo/--branch`. Exit codes: `0` va Upload a ZIP, a single HTML file, or a directory (auto-zipped) and get a stable URL back: +User and personal-key uploads are private by default. Service-key uploads default to public for automation. An explicit unsupported visibility is rejected before upload. Only the owner can replace, extend, delete, or change visibility. Public sites allow anyone with the link to view them. + +Use `openUrl` for a stable opening link. Making a public site private retires its old content URL; saved copies cannot be recalled. Named-user sharing is not included in this version. + ```bash lfc sites create ./report.html --name "perf report" # ✓ Created site a1b2c3d4e5 (perf report) # https://a1b2c3d4e5.sites.lifecycle.example.com lfc sites create ./dist # whole directory +lfc sites create ./dist --visibility public --yes lfc sites list --mine +lfc sites list --public --search report lfc sites get a1b2c3d4e5 lfc sites update a1b2c3d4e5 ./dist # replace content (new version) +lfc sites visibility a1b2c3d4e5 private --yes lfc sites extend a1b2c3d4e5 # push out the TTL/expiry lfc sites delete a1b2c3d4e5 --yes ``` @@ -185,7 +192,7 @@ Add a `.lfcsiteignore` file to the uploaded directory for additional ignore patt ```bash # Examples lfc builds get my-env --json | jq '.deploys[] | {name: .deployable.name, url: .publicUrl}' -lfc sites create ./coverage --json | jq -r .url +lfc sites create ./coverage --json | jq -r .openUrl lfc builds status my-env --watch && run-smoke-tests "$(lfc builds get my-env --json | jq -r '.deploys[0].publicUrl')" ``` diff --git a/src/commands/sites.ts b/src/commands/sites.ts index 4c30e4b..8bb21c1 100644 --- a/src/commands/sites.ts +++ b/src/commands/sites.ts @@ -1,64 +1,52 @@ import fs from 'node:fs'; import * as clack from '@clack/prompts'; -import { Command } from 'commander'; +import { Command, Option } from 'commander'; import pc from 'picocolors'; -import { decodeJwt } from '../lib/auth.js'; -import { loadTokens } from '../lib/config.js'; import { runAction, type Ctx } from '../lib/context.js'; import { formatAge, formatBytes, link, printJson, renderTable, statusColor } from '../lib/output.js'; +import { siteListView, validateSiteVisibility } from '../lib/sites.js'; +import type { Site, SitesCapabilities, SiteVisibility } from '../lib/types.js'; import { prepareSiteUpload, type PreparedSiteUpload } from '../lib/zip.js'; /** Build the multipart form for a site upload from a zip, html file, or directory. */ -async function uploadForm(upload: PreparedSiteUpload, name?: string): Promise { +async function uploadForm(upload: PreparedSiteUpload, name?: string, visibility?: SiteVisibility): Promise { const form = new FormData(); form.append('file', await fs.openAsBlob(upload.filePath, { type: upload.contentType }), upload.fileName); if (name) form.append('name', name); + if (visibility) form.append('visibility', visibility); return form; } -async function prepareUpload(ctx: Ctx, target: string): Promise { - let config; - try { - config = await ctx.api.getSitesConfig(); - } catch { - throw new Error('Could not load sites lifecycle. Try again later.'); - } - - return prepareSiteUpload(target, config); +async function prepareUpload(ctx: Ctx, target: string, capabilities?: SitesCapabilities): Promise { + return prepareSiteUpload(target, capabilities ?? (await ctx.api.getSitesCapabilities())); } -function userEmail(ctx: Ctx): string | undefined { - const tokens = loadTokens(ctx.profileName); - if (!tokens) return undefined; - try { - return decodeJwt(tokens.accessToken).email as string | undefined; - } catch { - return undefined; +async function confirmVisibility(visibility: SiteVisibility, yes?: boolean): Promise { + if (yes) return true; + if (!process.stdin.isTTY) throw new Error('Use --yes to confirm a visibility change in non-interactive mode'); + const ok = await clack.confirm({ + message: + visibility === 'public' + ? 'Publish this site so anyone with the link can view it?' + : 'Make this site private? Its current content URL will stop working. Saved copies cannot be recalled.', + }); + if (ok !== true) { + process.stderr.write('Aborted.\n'); + return false; } + return true; } -function printSite( - ctx: Ctx, - site: { - id: string; - url: string; - name?: string | null; - status: string; - expiresAt?: string | null; - fileCount?: number; - sizeBytes?: number; - }, - verb: string, -): void { +function printSite(ctx: Ctx, site: Site, verb: string): void { if (ctx.json) { printJson(site); return; } process.stderr.write(`${pc.green('✓')} ${verb} site ${pc.bold(site.id)}${site.name ? ` (${site.name})` : ''}\n`); - process.stdout.write(`${site.url}\n`); - const extras: string[] = []; + process.stdout.write(`${site.openUrl}\n`); + const extras: string[] = [site.visibility === 'private' ? 'private · only you' : 'public · anyone with the link']; if (site.fileCount != null) extras.push(`${site.fileCount} files`); if (site.sizeBytes != null) extras.push(formatBytes(site.sizeBytes)); if (site.expiresAt) extras.push(`expires ${new Date(site.expiresAt).toLocaleString()}`); @@ -74,51 +62,72 @@ export function registerSitesCommands(program: Command): void { sites .command('list') .description('List hosted sites') - .option('-m, --mine', 'only sites created/updated by me') + .option('-m, --mine', 'only sites owned by this identity') + .option('--public', 'only public sites') + .option('--search ', 'search site name or id') .option('-p, --page ', 'page number', v => Number(v), 1) .option('-n, --limit ', 'items per page', v => Number(v), 25) .action( - runAction(async (ctx, opts: { mine?: boolean; page: number; limit: number }) => { - const user = opts.mine ? userEmail(ctx) : undefined; - if (opts.mine && !user) throw new Error('Cannot resolve your email — log in first (`lfc login`)'); - const { items, pagination } = await ctx.api.listSites({ page: opts.page, limit: opts.limit, user }); - if (ctx.json) { - printJson({ sites: items, pagination }); - return; - } - if (items.length === 0) { - process.stdout.write(pc.dim('No sites found.\n')); - return; - } - const rows = items.map(s => [ - pc.bold(s.id), - s.name ?? '', - statusColor(s.status), - link(s.url), - formatBytes(s.sizeBytes), - s.expiresAt ? formatAge(s.expiresAt).replace(' ago', '') : '∞', - s.createdBy ?? '', - ]); - process.stdout.write( - renderTable(['id', 'name', 'status', 'url', 'size', 'expires in', 'created by'], rows) + '\n', - ); - if (pagination?.totalPages && Number(pagination.totalPages) > 1) { + runAction( + async (ctx, opts: { mine?: boolean; public?: boolean; search?: string; page: number; limit: number }) => { + const view = siteListView(opts); + const { items, pagination } = await ctx.api.listSites({ + page: opts.page, + limit: opts.limit, + view, + q: opts.search, + }); + if (ctx.json) { + printJson({ sites: items, pagination }); + return; + } + if (items.length === 0) { + process.stdout.write(pc.dim('No sites found.\n')); + return; + } + const rows = items.map(s => [ + pc.bold(s.id), + s.name ?? '', + statusColor(s.status), + link(s.openUrl), + formatBytes(s.sizeBytes), + s.expiresAt ? formatAge(s.expiresAt).replace(' ago', '') : '∞', + s.visibility, + s.currentRole === 'owner' ? 'owner' : 'view only', + ]); process.stdout.write( - pc.dim(`page ${pagination.page}/${pagination.totalPages} · ${pagination.totalItems} total\n`), + renderTable(['id', 'name', 'status', 'url', 'size', 'expires in', 'visibility', 'access'], rows) + '\n', ); - } - }), + if (pagination?.total && Number(pagination.total) > 1) { + process.stdout.write( + pc.dim(`page ${pagination.current}/${pagination.total} · ${pagination.items} total\n`), + ); + } + }, + ), ); sites .command('create ') .description('Upload a .zip, .html file, or directory and get back the site id + URL') .option('--name ', 'display name for the site') + .addOption( + new Option('--visibility ', 'private or public; defaults depend on your credential').choices([ + 'private', + 'public', + ]), + ) + .option('-y, --yes', 'confirm public publishing without a prompt') .action( - runAction(async (ctx, target: string, opts: { name?: string }) => { - const upload = await prepareUpload(ctx, target); + runAction(async (ctx, target: string, opts: { name?: string; visibility?: SiteVisibility; yes?: boolean }) => { + const capabilities = await ctx.api.getSitesCapabilities(); + if (!capabilities.enabled || !capabilities.canCreate) + throw new Error('Creating Sites is unavailable for this credential.'); + validateSiteVisibility(opts.visibility, capabilities); + if (opts.visibility === 'public' && !(await confirmVisibility('public', opts.yes))) return; + const upload = await prepareUpload(ctx, target, capabilities); try { - const form = await uploadForm(upload, opts.name); + const form = await uploadForm(upload, opts.name, opts.visibility); const site = await ctx.api.createSite(form); printSite(ctx, site, 'Created'); } finally { @@ -140,7 +149,10 @@ export function registerSitesCommands(program: Command): void { process.stdout.write(`${pc.bold(site.id)}${site.name ? ` ${site.name}` : ''}\n`); const fields: Array<[string, string]> = [ ['status', statusColor(site.status)], - ['url', link(site.url)], + ['url', link(site.openUrl)], + ['content url', link(site.contentUrl)], + ['visibility', site.visibility], + ['access', site.currentRole === 'owner' ? 'owner' : 'view only'], ['size', `${formatBytes(site.sizeBytes)}${site.fileCount != null ? ` (${site.fileCount} files)` : ''}`], ['created', `${site.createdAt ?? ''} ${pc.dim(site.createdBy ?? '')}`], ['updated', `${site.updatedAt ?? ''} ${pc.dim(site.updatedBy ?? '')}`], @@ -157,9 +169,14 @@ export function registerSitesCommands(program: Command): void { .description("Replace a site's content with a new .zip, .html file, or directory") .action( runAction(async (ctx, siteId: string, target: string) => { + const current = await ctx.api.getSite(siteId); + if (!current.permissions.canEdit) + throw new Error('Your current access does not allow you to replace its content.'); const upload = await prepareUpload(ctx, target); try { const form = await uploadForm(upload); + form.append('expectedAccessRevision', String(current.accessRevision)); + form.append('expectedContentRevision', String(current.contentRevision)); const site = await ctx.api.replaceSiteContent(siteId, form); printSite(ctx, site, 'Updated'); } finally { @@ -173,7 +190,10 @@ export function registerSitesCommands(program: Command): void { .description("Extend a site's expiration (TTL)") .action( runAction(async (ctx, siteId: string) => { - const site = await ctx.api.extendSite(siteId); + const current = await ctx.api.getSite(siteId); + if (!current.permissions.canEdit) + throw new Error('Your current access does not allow you to extend its expiry.'); + const site = await ctx.api.extendSite(siteId, current.accessRevision); if (ctx.json) printJson(site); else process.stderr.write( @@ -188,6 +208,9 @@ export function registerSitesCommands(program: Command): void { .option('-y, --yes', 'skip the confirmation prompt') .action( runAction(async (ctx, siteId: string, opts: { yes?: boolean }) => { + const current = await ctx.api.getSite(siteId); + if (!current.permissions.canDelete) + throw new Error('Your current access does not allow you to delete this site.'); if (!opts.yes) { if (!process.stdin.isTTY) throw new Error('Refusing to delete without --yes in non-interactive mode'); const ok = await clack.confirm({ message: `Delete site ${siteId}? Its URL stops working immediately.` }); @@ -196,9 +219,27 @@ export function registerSitesCommands(program: Command): void { return; } } - const site = await ctx.api.deleteSite(siteId); + const site = await ctx.api.deleteSite(siteId, current.accessRevision); if (ctx.json) printJson(site); else process.stderr.write(`${pc.green('✓')} Deleted site ${siteId}\n`); }), ); + sites + .command('visibility ') + .description('Publish a site or make it private (changes its content URL)') + .option('-y, --yes', 'skip the confirmation prompt') + .action( + runAction(async (ctx, siteId: string, value: string, opts: { yes?: boolean }) => { + if (value !== 'private' && value !== 'public') throw new Error('Visibility must be private or public.'); + const site = await ctx.api.getSite(siteId); + if (!site.permissions.canChangeVisibility) + throw new Error('Your current access does not allow you to change its visibility.'); + if (site.visibility === value) { + printSite(ctx, site, 'Unchanged'); + return; + } + if (!(await confirmVisibility(value, opts.yes))) return; + printSite(ctx, await ctx.api.setSiteVisibility(siteId, value, site.accessRevision), 'Updated'); + }), + ); } diff --git a/src/lib/api.ts b/src/lib/api.ts index 1ae5f76..8ab81e1 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,5 +1,6 @@ import { getAccessToken } from './auth.js'; import type { Profile } from './config.js'; +import type { ListSitesParams } from './generated/index.js'; import { matchBuilds, type Selector } from './resolve.js'; import type { ApiEnvelope, @@ -12,8 +13,8 @@ import type { PodInfo, ServiceOverrideState, Site, - SitesCliConfig, - SitesConfigCacheResponse, + SitesCapabilities, + SiteVisibility, WebhookInvocation, } from './types.js'; @@ -256,21 +257,24 @@ export class ApiClient { // --- sites --- - async listSites(params: { page?: number; limit?: number; user?: string }): Promise> { + async listSites(params: ListSitesParams): Promise> { const env = await this.request<{ sites: Site[] }>('GET', '/api/v2/sites', { - query: { page: params.page, limit: params.limit, user: params.user }, + query: { page: params.page, limit: params.limit, view: params.view, q: params.q }, }); return { items: env.data?.sites ?? [], pagination: env.metadata?.pagination }; } - async getSitesConfig(): Promise { - const env = await this.request('GET', '/api/v1/config/cache'); - const sites = env.data?.configs?.sites ?? env.data?.sites ?? env.data?.config?.sites ?? env.data?.data?.sites; - if (!sites) throw new ApiError('Could not load sites lifecycle. Try again later.', 0, env.request_id); - return { - enabled: sites.enabled, - upload: sites.upload, - }; + async getSitesCapabilities(): Promise { + const env = await this.request('GET', '/api/v2/sites/capabilities'); + if (!env.data) throw new ApiError('Could not load Sites capabilities.', 0, env.request_id); + return env.data; + } + + async setSiteVisibility(siteId: string, visibility: SiteVisibility, expectedAccessRevision: number): Promise { + const env = await this.request<{ site: Site }>('PATCH', `/api/v2/sites/${encodeURIComponent(siteId)}/access`, { + json: { visibility, expectedAccessRevision }, + }); + return (env.data as { site: Site }).site; } async createSite(form: FormData): Promise { @@ -283,13 +287,17 @@ export class ApiClient { return (env.data as { site: Site }).site; } - async deleteSite(siteId: string): Promise { - const env = await this.request<{ site: Site }>('DELETE', `/api/v2/sites/${encodeURIComponent(siteId)}`); + async deleteSite(siteId: string, expectedAccessRevision?: number): Promise { + const env = await this.request<{ site: Site }>('DELETE', `/api/v2/sites/${encodeURIComponent(siteId)}`, { + query: { expectedAccessRevision }, + }); return (env.data as { site: Site }).site; } - async extendSite(siteId: string): Promise { - const env = await this.request<{ site: Site }>('POST', `/api/v2/sites/${encodeURIComponent(siteId)}/extend`); + async extendSite(siteId: string, expectedAccessRevision?: number): Promise { + const env = await this.request<{ site: Site }>('POST', `/api/v2/sites/${encodeURIComponent(siteId)}/extend`, { + query: { expectedAccessRevision }, + }); return (env.data as { site: Site }).site; } diff --git a/src/lib/generated/bindSitesBrowserLogin200.ts b/src/lib/generated/bindSitesBrowserLogin200.ts new file mode 100644 index 0000000..1bb2fe0 --- /dev/null +++ b/src/lib/generated/bindSitesBrowserLogin200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { BindSitesBrowserLogin200Data } from './bindSitesBrowserLogin200Data.js'; + +export type BindSitesBrowserLogin200 = { + data: BindSitesBrowserLogin200Data; +}; diff --git a/src/lib/generated/bindSitesBrowserLogin200Data.ts b/src/lib/generated/bindSitesBrowserLogin200Data.ts new file mode 100644 index 0000000..83814bf --- /dev/null +++ b/src/lib/generated/bindSitesBrowserLogin200Data.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type BindSitesBrowserLogin200Data = { + bound: boolean; +}; diff --git a/src/lib/generated/deleteSiteParams.ts b/src/lib/generated/deleteSiteParams.ts new file mode 100644 index 0000000..5658bea --- /dev/null +++ b/src/lib/generated/deleteSiteParams.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type DeleteSiteParams = { +expectedAccessRevision?: number; +}; diff --git a/src/lib/generated/extendSiteParams.ts b/src/lib/generated/extendSiteParams.ts new file mode 100644 index 0000000..d92e074 --- /dev/null +++ b/src/lib/generated/extendSiteParams.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type ExtendSiteParams = { +expectedAccessRevision?: number; +}; diff --git a/src/lib/generated/index.ts b/src/lib/generated/index.ts index 5533ac0..a3b32b3 100644 --- a/src/lib/generated/index.ts +++ b/src/lib/generated/index.ts @@ -812,4 +812,35 @@ export * from './workspaceRuntimeFailureOrigin.js'; export * from './workspaceRuntimeFailureStage.js'; export * from './workspaceRuntimeProviderSelectedService.js'; export * from './workspaceRuntimeProviderState.js'; -export * from './workspaceRuntimeProviderWorkspaceStorage.js'; \ No newline at end of file +export * from './workspaceRuntimeProviderWorkspaceStorage.js'; +export * from './deleteSiteParams.js'; +export * from './extendSiteParams.js'; +export * from './listSitesView.js'; +export * from './setSiteVisibilityBody.js'; +export * from './setSiteVisibilityBodyVisibility.js'; +export * from './siteCurrentRole.js'; +export * from './sitePermissions.js'; +export * from './siteUploadRequestVisibility.js'; +export * from './siteVisibility.js'; +export * from './sitesCapabilities.js'; +export * from './sitesCapabilitiesAllowedVisibilitiesItem.js'; +export * from './sitesCapabilitiesDefaultVisibility.js'; +export * from './sitesCapabilitiesSuccessResponse.js'; +export * from './sitesCapabilitiesSuccessResponseAllOf.js'; +export * from './sitesCapabilitiesUpload.js'; +export * from './bindSitesBrowserLogin200.js'; +export * from './bindSitesBrowserLogin200Data.js'; +export * from './mintSitesBrowserLogin200.js'; +export * from './mintSitesBrowserLogin200Data.js'; +export * from './resolvePublicSiteBrowserUrl200.js'; +export * from './resolvePublicSiteBrowserUrl200Data.js'; +export * from './resolvePublicSiteBrowserUrl404.js'; +export * from './resolvePublicSiteBrowserUrl404Error.js'; +export * from './resolvePublicSiteBrowserUrl404ErrorMessage.js'; +export * from './revokeSitesBrowserLogin200.js'; +export * from './revokeSitesBrowserLogin200Data.js'; +export * from './sitesBrowserBindRequest.js'; +export * from './sitesBrowserBindRequestAllOf.js'; +export * from './sitesBrowserLoginRequest.js'; +export * from './sitesBrowserMintRequest.js'; +export * from './sitesBrowserMintRequestAllOf.js'; diff --git a/src/lib/generated/listSitesParams.ts b/src/lib/generated/listSitesParams.ts index f040f96..0065661 100644 --- a/src/lib/generated/listSitesParams.ts +++ b/src/lib/generated/listSitesParams.ts @@ -5,12 +5,11 @@ * API documentation for lifecycle * OpenAPI spec version: 2.0.0 */ +import type { ListSitesView } from './listSitesView.js'; export type ListSitesParams = { -/** - * Filters to sites created or last updated by the supplied user email. - */ -user?: string; +view?: ListSitesView; +q?: string; /** * Page number for pagination. */ diff --git a/src/lib/generated/listSitesView.ts b/src/lib/generated/listSitesView.ts new file mode 100644 index 0000000..8d3ba8a --- /dev/null +++ b/src/lib/generated/listSitesView.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type ListSitesView = typeof ListSitesView[keyof typeof ListSitesView]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const ListSitesView = { + mine: 'mine', + public: 'public', + all: 'all', +} as const; diff --git a/src/lib/generated/mintSitesBrowserLogin200.ts b/src/lib/generated/mintSitesBrowserLogin200.ts new file mode 100644 index 0000000..3014dc3 --- /dev/null +++ b/src/lib/generated/mintSitesBrowserLogin200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { MintSitesBrowserLogin200Data } from './mintSitesBrowserLogin200Data.js'; + +export type MintSitesBrowserLogin200 = { + data: MintSitesBrowserLogin200Data; +}; diff --git a/src/lib/generated/mintSitesBrowserLogin200Data.ts b/src/lib/generated/mintSitesBrowserLogin200Data.ts new file mode 100644 index 0000000..d595e0d --- /dev/null +++ b/src/lib/generated/mintSitesBrowserLogin200Data.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type MintSitesBrowserLogin200Data = { + ticket: string; + consumeUrl: string; +}; diff --git a/src/lib/generated/resolvePublicSiteBrowserUrl200.ts b/src/lib/generated/resolvePublicSiteBrowserUrl200.ts new file mode 100644 index 0000000..4b5c43e --- /dev/null +++ b/src/lib/generated/resolvePublicSiteBrowserUrl200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { ResolvePublicSiteBrowserUrl200Data } from './resolvePublicSiteBrowserUrl200Data.js'; + +export type ResolvePublicSiteBrowserUrl200 = { + data: ResolvePublicSiteBrowserUrl200Data; +}; diff --git a/src/lib/generated/resolvePublicSiteBrowserUrl200Data.ts b/src/lib/generated/resolvePublicSiteBrowserUrl200Data.ts new file mode 100644 index 0000000..edeefe9 --- /dev/null +++ b/src/lib/generated/resolvePublicSiteBrowserUrl200Data.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type ResolvePublicSiteBrowserUrl200Data = { + url: string; +}; diff --git a/src/lib/generated/resolvePublicSiteBrowserUrl404.ts b/src/lib/generated/resolvePublicSiteBrowserUrl404.ts new file mode 100644 index 0000000..e35384b --- /dev/null +++ b/src/lib/generated/resolvePublicSiteBrowserUrl404.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { ResolvePublicSiteBrowserUrl404Error } from './resolvePublicSiteBrowserUrl404Error.js'; + +export type ResolvePublicSiteBrowserUrl404 = { + error: ResolvePublicSiteBrowserUrl404Error; +}; diff --git a/src/lib/generated/resolvePublicSiteBrowserUrl404Error.ts b/src/lib/generated/resolvePublicSiteBrowserUrl404Error.ts new file mode 100644 index 0000000..4d1ebb7 --- /dev/null +++ b/src/lib/generated/resolvePublicSiteBrowserUrl404Error.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { ResolvePublicSiteBrowserUrl404ErrorMessage } from './resolvePublicSiteBrowserUrl404ErrorMessage.js'; + +export type ResolvePublicSiteBrowserUrl404Error = { + message: ResolvePublicSiteBrowserUrl404ErrorMessage; +}; diff --git a/src/lib/generated/resolvePublicSiteBrowserUrl404ErrorMessage.ts b/src/lib/generated/resolvePublicSiteBrowserUrl404ErrorMessage.ts new file mode 100644 index 0000000..f263c9e --- /dev/null +++ b/src/lib/generated/resolvePublicSiteBrowserUrl404ErrorMessage.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type ResolvePublicSiteBrowserUrl404ErrorMessage = typeof ResolvePublicSiteBrowserUrl404ErrorMessage[keyof typeof ResolvePublicSiteBrowserUrl404ErrorMessage]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const ResolvePublicSiteBrowserUrl404ErrorMessage = { + Site_not_found: 'Site not found.', +} as const; diff --git a/src/lib/generated/revokeSitesBrowserLogin200.ts b/src/lib/generated/revokeSitesBrowserLogin200.ts new file mode 100644 index 0000000..16d5620 --- /dev/null +++ b/src/lib/generated/revokeSitesBrowserLogin200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { RevokeSitesBrowserLogin200Data } from './revokeSitesBrowserLogin200Data.js'; + +export type RevokeSitesBrowserLogin200 = { + data: RevokeSitesBrowserLogin200Data; +}; diff --git a/src/lib/generated/revokeSitesBrowserLogin200Data.ts b/src/lib/generated/revokeSitesBrowserLogin200Data.ts new file mode 100644 index 0000000..b875483 --- /dev/null +++ b/src/lib/generated/revokeSitesBrowserLogin200Data.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type RevokeSitesBrowserLogin200Data = { + revoked: boolean; +}; diff --git a/src/lib/generated/setSiteVisibilityBody.ts b/src/lib/generated/setSiteVisibilityBody.ts new file mode 100644 index 0000000..117960f --- /dev/null +++ b/src/lib/generated/setSiteVisibilityBody.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { SetSiteVisibilityBodyVisibility } from './setSiteVisibilityBodyVisibility.js'; + +export type SetSiteVisibilityBody = { + visibility: SetSiteVisibilityBodyVisibility; + /** + * @minimum 1 + * @maximum 2147483647 + */ + expectedAccessRevision: number; +}; diff --git a/src/lib/generated/setSiteVisibilityBodyVisibility.ts b/src/lib/generated/setSiteVisibilityBodyVisibility.ts new file mode 100644 index 0000000..f77066f --- /dev/null +++ b/src/lib/generated/setSiteVisibilityBodyVisibility.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SetSiteVisibilityBodyVisibility = typeof SetSiteVisibilityBodyVisibility[keyof typeof SetSiteVisibilityBodyVisibility]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SetSiteVisibilityBodyVisibility = { + private: 'private', + public: 'public', +} as const; diff --git a/src/lib/generated/site.ts b/src/lib/generated/site.ts index 29b218c..8e2a9b5 100644 --- a/src/lib/generated/site.ts +++ b/src/lib/generated/site.ts @@ -5,12 +5,28 @@ * API documentation for lifecycle * OpenAPI spec version: 2.0.0 */ +import type { SiteVisibility } from './siteVisibility.js'; +import type { SiteCurrentRole } from './siteCurrentRole.js'; +import type { SitePermissions } from './sitePermissions.js'; import type { SiteStatus } from './siteStatus.js'; export interface Site { id: string; name: string; url: string; + visibility: SiteVisibility; + contentUrl: string; + openUrl: string; + /** + * Site mutation revision; advances on content replacement, visibility, expiry extension and deletion. + * @minimum 1 + */ + accessRevision: number; + /** @minimum 1 */ + contentRevision: number; + /** @nullable */ + currentRole: SiteCurrentRole; + permissions: SitePermissions; status: SiteStatus; /** @nullable */ createdAt: string | null; diff --git a/src/lib/generated/siteCurrentRole.ts b/src/lib/generated/siteCurrentRole.ts new file mode 100644 index 0000000..b513b56 --- /dev/null +++ b/src/lib/generated/siteCurrentRole.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +/** + * @nullable + */ +export type SiteCurrentRole = typeof SiteCurrentRole[keyof typeof SiteCurrentRole] | null; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SiteCurrentRole = { + owner: 'owner', +} as const; diff --git a/src/lib/generated/sitePermissions.ts b/src/lib/generated/sitePermissions.ts new file mode 100644 index 0000000..ea48c03 --- /dev/null +++ b/src/lib/generated/sitePermissions.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SitePermissions = { + canView: boolean; + canEdit: boolean; + canDelete: boolean; + canChangeVisibility: boolean; +}; diff --git a/src/lib/generated/siteUploadRequest.ts b/src/lib/generated/siteUploadRequest.ts index 92ef6d6..02a22db 100644 --- a/src/lib/generated/siteUploadRequest.ts +++ b/src/lib/generated/siteUploadRequest.ts @@ -5,8 +5,21 @@ * API documentation for lifecycle * OpenAPI spec version: 2.0.0 */ +import type { SiteUploadRequestVisibility } from './siteUploadRequestVisibility.js'; export interface SiteUploadRequest { + /** Creation only; omitted defaults private for users. Service-key creation always remains public. */ + visibility?: SiteUploadRequestVisibility; + /** + * @minimum 1 + * @maximum 2147483647 + */ + expectedAccessRevision?: number; + /** + * @minimum 1 + * @maximum 2147483647 + */ + expectedContentRevision?: number; /** Static HTML file, supported asset file, or ZIP archive containing index.html. */ file: Blob; /** Optional display name for the hosted site. */ diff --git a/src/lib/generated/siteUploadRequestVisibility.ts b/src/lib/generated/siteUploadRequestVisibility.ts new file mode 100644 index 0000000..c097ba3 --- /dev/null +++ b/src/lib/generated/siteUploadRequestVisibility.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +/** + * Creation only; omitted defaults private for users. Service-key creation always remains public. + */ +export type SiteUploadRequestVisibility = typeof SiteUploadRequestVisibility[keyof typeof SiteUploadRequestVisibility]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SiteUploadRequestVisibility = { + private: 'private', + public: 'public', +} as const; diff --git a/src/lib/generated/siteVisibility.ts b/src/lib/generated/siteVisibility.ts new file mode 100644 index 0000000..890cc7d --- /dev/null +++ b/src/lib/generated/siteVisibility.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SiteVisibility = typeof SiteVisibility[keyof typeof SiteVisibility]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SiteVisibility = { + private: 'private', + public: 'public', +} as const; diff --git a/src/lib/generated/sitesBrowserBindRequest.ts b/src/lib/generated/sitesBrowserBindRequest.ts new file mode 100644 index 0000000..38ea7f7 --- /dev/null +++ b/src/lib/generated/sitesBrowserBindRequest.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { SitesBrowserLoginRequest } from './sitesBrowserLoginRequest.js'; +import type { SitesBrowserBindRequestAllOf } from './sitesBrowserBindRequestAllOf.js'; + +export type SitesBrowserBindRequest = SitesBrowserLoginRequest & SitesBrowserBindRequestAllOf; diff --git a/src/lib/generated/sitesBrowserBindRequestAllOf.ts b/src/lib/generated/sitesBrowserBindRequestAllOf.ts new file mode 100644 index 0000000..5517550 --- /dev/null +++ b/src/lib/generated/sitesBrowserBindRequestAllOf.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SitesBrowserBindRequestAllOf = { + /** True only for an explicit OAuth sign-in with a newly generated login nonce. Refresh requests must omit this or send false and match an existing unrevoked login. */ + createLogin?: boolean; +}; diff --git a/src/lib/generated/sitesBrowserLoginRequest.ts b/src/lib/generated/sitesBrowserLoginRequest.ts new file mode 100644 index 0000000..ba98a87 --- /dev/null +++ b/src/lib/generated/sitesBrowserLoginRequest.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export interface SitesBrowserLoginRequest { + /** + * Server-generated application-login nonce, distinct from the identity provider SSO session. + * @pattern ^[A-Za-z0-9_-]{43}$ + */ + loginId: string; + /** Absolute application-login expiry in Unix seconds. Binding and minting require a future expiry; revocation also accepts an expired login. The server limits future expiry to its login lifetime. */ + loginExpiresAt: number; +} diff --git a/src/lib/generated/sitesBrowserMintRequest.ts b/src/lib/generated/sitesBrowserMintRequest.ts new file mode 100644 index 0000000..adfb9a0 --- /dev/null +++ b/src/lib/generated/sitesBrowserMintRequest.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { SitesBrowserLoginRequest } from './sitesBrowserLoginRequest.js'; +import type { SitesBrowserMintRequestAllOf } from './sitesBrowserMintRequestAllOf.js'; + +export type SitesBrowserMintRequest = SitesBrowserLoginRequest & SitesBrowserMintRequestAllOf; diff --git a/src/lib/generated/sitesBrowserMintRequestAllOf.ts b/src/lib/generated/sitesBrowserMintRequestAllOf.ts new file mode 100644 index 0000000..5f71221 --- /dev/null +++ b/src/lib/generated/sitesBrowserMintRequestAllOf.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SitesBrowserMintRequestAllOf = { + /** + * Opaque, unexpired bootstrap challenge issued by the content gateway. + * @pattern ^[A-Za-z0-9_-]{43}$ + */ + state: string; + /** Site identifier matching the bootstrap challenge. */ + siteId: string; +}; diff --git a/src/lib/generated/sitesCapabilities.ts b/src/lib/generated/sitesCapabilities.ts new file mode 100644 index 0000000..4bb8acd --- /dev/null +++ b/src/lib/generated/sitesCapabilities.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { SitesCapabilitiesDefaultVisibility } from './sitesCapabilitiesDefaultVisibility.js'; +import type { SitesCapabilitiesAllowedVisibilitiesItem } from './sitesCapabilitiesAllowedVisibilitiesItem.js'; +import type { SitesCapabilitiesUpload } from './sitesCapabilitiesUpload.js'; + +export interface SitesCapabilities { + enabled: boolean; + canCreate: boolean; + defaultVisibility: SitesCapabilitiesDefaultVisibility; + allowedVisibilities: SitesCapabilitiesAllowedVisibilitiesItem[]; + upload: SitesCapabilitiesUpload; +} diff --git a/src/lib/generated/sitesCapabilitiesAllowedVisibilitiesItem.ts b/src/lib/generated/sitesCapabilitiesAllowedVisibilitiesItem.ts new file mode 100644 index 0000000..813d1f5 --- /dev/null +++ b/src/lib/generated/sitesCapabilitiesAllowedVisibilitiesItem.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SitesCapabilitiesAllowedVisibilitiesItem = typeof SitesCapabilitiesAllowedVisibilitiesItem[keyof typeof SitesCapabilitiesAllowedVisibilitiesItem]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SitesCapabilitiesAllowedVisibilitiesItem = { + private: 'private', + public: 'public', +} as const; diff --git a/src/lib/generated/sitesCapabilitiesDefaultVisibility.ts b/src/lib/generated/sitesCapabilitiesDefaultVisibility.ts new file mode 100644 index 0000000..e5df59d --- /dev/null +++ b/src/lib/generated/sitesCapabilitiesDefaultVisibility.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SitesCapabilitiesDefaultVisibility = typeof SitesCapabilitiesDefaultVisibility[keyof typeof SitesCapabilitiesDefaultVisibility]; + + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const SitesCapabilitiesDefaultVisibility = { + private: 'private', + public: 'public', +} as const; diff --git a/src/lib/generated/sitesCapabilitiesSuccessResponse.ts b/src/lib/generated/sitesCapabilitiesSuccessResponse.ts new file mode 100644 index 0000000..e44da44 --- /dev/null +++ b/src/lib/generated/sitesCapabilitiesSuccessResponse.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { SuccessApiResponse } from './successApiResponse.js'; +import type { SitesCapabilitiesSuccessResponseAllOf } from './sitesCapabilitiesSuccessResponseAllOf.js'; + +export type SitesCapabilitiesSuccessResponse = SuccessApiResponse & SitesCapabilitiesSuccessResponseAllOf; diff --git a/src/lib/generated/sitesCapabilitiesSuccessResponseAllOf.ts b/src/lib/generated/sitesCapabilitiesSuccessResponseAllOf.ts new file mode 100644 index 0000000..4e2bab6 --- /dev/null +++ b/src/lib/generated/sitesCapabilitiesSuccessResponseAllOf.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { SitesCapabilities } from './sitesCapabilities.js'; + +export type SitesCapabilitiesSuccessResponseAllOf = { + data: SitesCapabilities; +}; diff --git a/src/lib/generated/sitesCapabilitiesUpload.ts b/src/lib/generated/sitesCapabilitiesUpload.ts new file mode 100644 index 0000000..79c3a53 --- /dev/null +++ b/src/lib/generated/sitesCapabilitiesUpload.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ + +export type SitesCapabilitiesUpload = { + /** @minimum 1 */ + maxUploadBytes: number; + /** @minimum 1 */ + maxExtractedBytes: number; + /** @minimum 1 */ + maxFiles: number; + allowedExtensions: string[]; +}; diff --git a/src/lib/sites.ts b/src/lib/sites.ts new file mode 100644 index 0000000..1ca3db1 --- /dev/null +++ b/src/lib/sites.ts @@ -0,0 +1,11 @@ +import type { ListSitesView } from './generated/index.js'; +import type { SitesCapabilities, SiteVisibility } from './types.js'; + +export function siteListView(opts: { mine?: boolean; public?: boolean }): ListSitesView { + if (opts.mine && opts.public) throw new Error('--mine and --public cannot be combined.'); + return opts.mine ? 'mine' : opts.public ? 'public' : 'all'; +} +export function validateSiteVisibility(visibility: SiteVisibility | undefined, capabilities: SitesCapabilities): void { + if (visibility && !capabilities.allowedVisibilities.includes(visibility)) + throw new Error(`Visibility ${visibility} is not supported for this credential. No files were uploaded.`); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index e8d02ba..6ae9c99 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -6,7 +6,6 @@ import type { Deploy as GeneratedDeploy, Deployable as GeneratedDeployable, PaginationMetadata as GeneratedPaginationMetadata, - SitesConfig as GeneratedSitesConfig, LogStreamResponse, NativeBuildJobInfo, PullRequest, @@ -36,12 +35,7 @@ export interface SitesCliConfig { upload: SitesUploadConfig; } -export interface SitesConfigCacheResponse { - configs?: { sites?: GeneratedSitesConfig }; - sites?: GeneratedSitesConfig; - config?: { sites?: GeneratedSitesConfig }; - data?: { sites?: GeneratedSitesConfig }; -} +export type { SiteVisibility, SitesCapabilities } from './generated/index.js'; export type PullRequestSummary = PullRequest; diff --git a/tests/sites-command.test.ts b/tests/sites-command.test.ts new file mode 100644 index 0000000..e2c0e7b --- /dev/null +++ b/tests/sites-command.test.ts @@ -0,0 +1,47 @@ +import { Command } from 'commander'; +import { beforeEach, expect, it, vi } from 'vitest'; + +import { registerSitesCommands } from '../src/commands/sites.js'; + +const api = vi.hoisted(() => ({ getSite: vi.fn(), getSitesCapabilities: vi.fn(), setSiteVisibility: vi.fn() })); +vi.mock('../src/lib/context.js', () => ({ + runAction: + (action: (ctx: unknown, ...args: unknown[]) => Promise) => + (...args: unknown[]) => + action({ api, json: true }, ...args.slice(0, -1)), +})); +vi.mock('../src/lib/output.js', async importOriginal => ({ + ...(await importOriginal()), + printJson: vi.fn(), +})); +beforeEach(() => { + vi.clearAllMocks(); + api.getSite.mockResolvedValue({ + id: 'owned', + visibility: 'private', + accessRevision: 7, + permissions: { canChangeVisibility: true }, + }); + api.getSitesCapabilities.mockResolvedValue({ canCreate: false, allowedVisibilities: [] }); + api.setSiteVisibility.mockResolvedValue({ id: 'owned', visibility: 'public' }); +}); +async function run() { + const command = new Command(); + registerSitesCommands(command); + await command.parseAsync(['sites', 'visibility', 'owned', 'public', '--yes'], { from: 'user' }); +} +it('publishes an owned existing site even when creation capability is unavailable', async () => { + await run(); + expect(api.setSiteVisibility).toHaveBeenCalledWith('owned', 'public', 7); + expect(api.getSitesCapabilities).not.toHaveBeenCalled(); +}); +it('does not mutate when the current Site permission denies visibility changes', async () => { + api.getSite.mockResolvedValue({ visibility: 'private', permissions: { canChangeVisibility: false } }); + await expect(run()).rejects.toThrow('current access'); + expect(api.setSiteVisibility).not.toHaveBeenCalled(); +}); +it('propagates a stale-revision rejection from the server without retrying publication', async () => { + api.setSiteVisibility.mockRejectedValue(new Error('Site changed')); + await expect(run()).rejects.toThrow('Site changed'); + expect(api.setSiteVisibility).toHaveBeenCalledTimes(1); +}); diff --git a/tests/sites.test.ts b/tests/sites.test.ts new file mode 100644 index 0000000..6668b9f --- /dev/null +++ b/tests/sites.test.ts @@ -0,0 +1,78 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { ApiClient } from '../src/lib/api.js'; +import { siteListView, validateSiteVisibility } from '../src/lib/sites.js'; +import type { SitesCapabilities } from '../src/lib/types.js'; + +vi.mock('../src/lib/auth.js', () => ({ getAccessToken: vi.fn(async () => 'lfc_test_personal_key') })); +const client = new ApiClient('fixture', { apiUrl: 'https://example.test', authEnabled: true }); +const capabilities: SitesCapabilities = { + enabled: true, + canCreate: true, + defaultVisibility: 'public', + allowedVisibilities: ['public'], + upload: { maxUploadBytes: 100, maxExtractedBytes: 100, maxFiles: 3, allowedExtensions: ['html'] }, +}; +afterEach(() => vi.unstubAllGlobals()); +function mockResponse(data: unknown) { + const fetchMock = vi.fn( + async () => + new Response(JSON.stringify({ request_id: 'test', data, error: null }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + vi.stubGlobal('fetch', fetchMock); + return fetchMock; +} +describe('Sites V1 contracts', () => { + it('uses a principal authenticated capabilities route, never legacy config cache', async () => { + const fetchMock = mockResponse(capabilities); + expect(await client.getSitesCapabilities()).toEqual(capabilities); + const [url, options] = fetchMock.mock.calls[0] as unknown as [URL, RequestInit]; + expect(url.pathname).toBe('/api/v2/sites/capabilities'); + expect(options.headers).toEqual({ Authorization: 'Bearer lfc_test_personal_key' }); + }); + it('maps mine to server identity instead of requiring email', async () => { + const fetchMock = mockResponse({ sites: [] }); + await client.listSites({ view: siteListView({ mine: true }), q: 'report & notes', page: 3, limit: 50 }); + const url = fetchMock.mock.calls[0]![0] as unknown as URL; + expect(Object.fromEntries(url.searchParams)).toEqual({ view: 'mine', q: 'report & notes', page: '3', limit: '50' }); + }); + it('rejects contradictory list filters', () => + expect(() => siteListView({ mine: true, public: true })).toThrow('cannot be combined')); + it('allows omitted visibility to use the server credential default and rejects unsupported private requests', () => { + expect(() => validateSiteVisibility(undefined, capabilities)).not.toThrow(); + expect(() => validateSiteVisibility('private', capabilities)).toThrow('No files were uploaded'); + }); + it('sends visibility preconditions and keeps the site locator escaped', async () => { + const fetchMock = mockResponse({ site: { id: 'test' } }); + await client.setSiteVisibility('test/id', 'private', 7); + const [url, options] = fetchMock.mock.calls[0] as unknown as [URL, RequestInit]; + expect(url.pathname).toBe('/api/v2/sites/test%2Fid/access'); + expect(options.method).toBe('PATCH'); + expect(JSON.parse(String(options.body))).toEqual({ visibility: 'private', expectedAccessRevision: 7 }); + }); + it('preserves server errors such as stale visibility conflicts', async () => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ error: { message: 'Access changed', code: 'stale_revision' }, data: null }), { + status: 409, + }), + ), + ); + await expect(client.setSiteVisibility('test', 'public', 1)).rejects.toMatchObject({ + status: 409, + code: 'stale_revision', + }); + }); + it('sends mutation revisions on delete and extend', async () => { + const fetchMock = mockResponse({ site: { id: 'test' } }); + await client.deleteSite('test', 8); + await client.extendSite('test', 9); + expect((fetchMock.mock.calls[0]![0] as unknown as URL).searchParams.get('expectedAccessRevision')).toBe('8'); + expect((fetchMock.mock.calls[1]![0] as unknown as URL).searchParams.get('expectedAccessRevision')).toBe('9'); + }); +}); From 6814da76083fa989ffcc967e433b931d40f1dd7f Mon Sep 17 00:00:00 2001 From: vigneshrajsb Date: Tue, 15 Sep 2026 17:15:52 -0700 Subject: [PATCH 2/6] chore(sites): refresh simplified browser api types --- src/lib/generated/bindSitesBrowserLogin200.ts | 12 ------------ .../generated/bindSitesBrowserLogin200Data.ts | 11 ----------- src/lib/generated/index.ts | 12 ++---------- src/lib/generated/mintSitesBrowserLogin200.ts | 12 ------------ src/lib/generated/mintSitesBrowserTicket200.ts | 12 ++++++++++++ ...Data.ts => mintSitesBrowserTicket200Data.ts} | 2 +- src/lib/generated/revokeSitesBrowserLogin200.ts | 12 ------------ .../generated/revokeSitesBrowserLogin200Data.ts | 11 ----------- src/lib/generated/sitesBrowserBindRequest.ts | 11 ----------- .../generated/sitesBrowserBindRequestAllOf.ts | 12 ------------ src/lib/generated/sitesBrowserLoginRequest.ts | 17 ----------------- src/lib/generated/sitesBrowserMintRequest.ts | 15 ++++++++++++--- .../generated/sitesBrowserMintRequestAllOf.ts | 17 ----------------- 13 files changed, 27 insertions(+), 129 deletions(-) delete mode 100644 src/lib/generated/bindSitesBrowserLogin200.ts delete mode 100644 src/lib/generated/bindSitesBrowserLogin200Data.ts delete mode 100644 src/lib/generated/mintSitesBrowserLogin200.ts create mode 100644 src/lib/generated/mintSitesBrowserTicket200.ts rename src/lib/generated/{mintSitesBrowserLogin200Data.ts => mintSitesBrowserTicket200Data.ts} (80%) delete mode 100644 src/lib/generated/revokeSitesBrowserLogin200.ts delete mode 100644 src/lib/generated/revokeSitesBrowserLogin200Data.ts delete mode 100644 src/lib/generated/sitesBrowserBindRequest.ts delete mode 100644 src/lib/generated/sitesBrowserBindRequestAllOf.ts delete mode 100644 src/lib/generated/sitesBrowserLoginRequest.ts delete mode 100644 src/lib/generated/sitesBrowserMintRequestAllOf.ts diff --git a/src/lib/generated/bindSitesBrowserLogin200.ts b/src/lib/generated/bindSitesBrowserLogin200.ts deleted file mode 100644 index 1bb2fe0..0000000 --- a/src/lib/generated/bindSitesBrowserLogin200.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ -import type { BindSitesBrowserLogin200Data } from './bindSitesBrowserLogin200Data.js'; - -export type BindSitesBrowserLogin200 = { - data: BindSitesBrowserLogin200Data; -}; diff --git a/src/lib/generated/bindSitesBrowserLogin200Data.ts b/src/lib/generated/bindSitesBrowserLogin200Data.ts deleted file mode 100644 index 83814bf..0000000 --- a/src/lib/generated/bindSitesBrowserLogin200Data.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ - -export type BindSitesBrowserLogin200Data = { - bound: boolean; -}; diff --git a/src/lib/generated/index.ts b/src/lib/generated/index.ts index a3b32b3..d8520e6 100644 --- a/src/lib/generated/index.ts +++ b/src/lib/generated/index.ts @@ -828,19 +828,11 @@ export * from './sitesCapabilitiesDefaultVisibility.js'; export * from './sitesCapabilitiesSuccessResponse.js'; export * from './sitesCapabilitiesSuccessResponseAllOf.js'; export * from './sitesCapabilitiesUpload.js'; -export * from './bindSitesBrowserLogin200.js'; -export * from './bindSitesBrowserLogin200Data.js'; -export * from './mintSitesBrowserLogin200.js'; -export * from './mintSitesBrowserLogin200Data.js'; export * from './resolvePublicSiteBrowserUrl200.js'; export * from './resolvePublicSiteBrowserUrl200Data.js'; export * from './resolvePublicSiteBrowserUrl404.js'; export * from './resolvePublicSiteBrowserUrl404Error.js'; export * from './resolvePublicSiteBrowserUrl404ErrorMessage.js'; -export * from './revokeSitesBrowserLogin200.js'; -export * from './revokeSitesBrowserLogin200Data.js'; -export * from './sitesBrowserBindRequest.js'; -export * from './sitesBrowserBindRequestAllOf.js'; -export * from './sitesBrowserLoginRequest.js'; +export * from './mintSitesBrowserTicket200.js'; +export * from './mintSitesBrowserTicket200Data.js'; export * from './sitesBrowserMintRequest.js'; -export * from './sitesBrowserMintRequestAllOf.js'; diff --git a/src/lib/generated/mintSitesBrowserLogin200.ts b/src/lib/generated/mintSitesBrowserLogin200.ts deleted file mode 100644 index 3014dc3..0000000 --- a/src/lib/generated/mintSitesBrowserLogin200.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ -import type { MintSitesBrowserLogin200Data } from './mintSitesBrowserLogin200Data.js'; - -export type MintSitesBrowserLogin200 = { - data: MintSitesBrowserLogin200Data; -}; diff --git a/src/lib/generated/mintSitesBrowserTicket200.ts b/src/lib/generated/mintSitesBrowserTicket200.ts new file mode 100644 index 0000000..030bad2 --- /dev/null +++ b/src/lib/generated/mintSitesBrowserTicket200.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.4.1 🍺 + * Do not edit manually. + * Lifecycle API + * API documentation for lifecycle + * OpenAPI spec version: 2.0.0 + */ +import type { MintSitesBrowserTicket200Data } from './mintSitesBrowserTicket200Data.js'; + +export type MintSitesBrowserTicket200 = { + data: MintSitesBrowserTicket200Data; +}; diff --git a/src/lib/generated/mintSitesBrowserLogin200Data.ts b/src/lib/generated/mintSitesBrowserTicket200Data.ts similarity index 80% rename from src/lib/generated/mintSitesBrowserLogin200Data.ts rename to src/lib/generated/mintSitesBrowserTicket200Data.ts index d595e0d..aecd141 100644 --- a/src/lib/generated/mintSitesBrowserLogin200Data.ts +++ b/src/lib/generated/mintSitesBrowserTicket200Data.ts @@ -6,7 +6,7 @@ * OpenAPI spec version: 2.0.0 */ -export type MintSitesBrowserLogin200Data = { +export type MintSitesBrowserTicket200Data = { ticket: string; consumeUrl: string; }; diff --git a/src/lib/generated/revokeSitesBrowserLogin200.ts b/src/lib/generated/revokeSitesBrowserLogin200.ts deleted file mode 100644 index 16d5620..0000000 --- a/src/lib/generated/revokeSitesBrowserLogin200.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ -import type { RevokeSitesBrowserLogin200Data } from './revokeSitesBrowserLogin200Data.js'; - -export type RevokeSitesBrowserLogin200 = { - data: RevokeSitesBrowserLogin200Data; -}; diff --git a/src/lib/generated/revokeSitesBrowserLogin200Data.ts b/src/lib/generated/revokeSitesBrowserLogin200Data.ts deleted file mode 100644 index b875483..0000000 --- a/src/lib/generated/revokeSitesBrowserLogin200Data.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ - -export type RevokeSitesBrowserLogin200Data = { - revoked: boolean; -}; diff --git a/src/lib/generated/sitesBrowserBindRequest.ts b/src/lib/generated/sitesBrowserBindRequest.ts deleted file mode 100644 index 38ea7f7..0000000 --- a/src/lib/generated/sitesBrowserBindRequest.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ -import type { SitesBrowserLoginRequest } from './sitesBrowserLoginRequest.js'; -import type { SitesBrowserBindRequestAllOf } from './sitesBrowserBindRequestAllOf.js'; - -export type SitesBrowserBindRequest = SitesBrowserLoginRequest & SitesBrowserBindRequestAllOf; diff --git a/src/lib/generated/sitesBrowserBindRequestAllOf.ts b/src/lib/generated/sitesBrowserBindRequestAllOf.ts deleted file mode 100644 index 5517550..0000000 --- a/src/lib/generated/sitesBrowserBindRequestAllOf.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ - -export type SitesBrowserBindRequestAllOf = { - /** True only for an explicit OAuth sign-in with a newly generated login nonce. Refresh requests must omit this or send false and match an existing unrevoked login. */ - createLogin?: boolean; -}; diff --git a/src/lib/generated/sitesBrowserLoginRequest.ts b/src/lib/generated/sitesBrowserLoginRequest.ts deleted file mode 100644 index ba98a87..0000000 --- a/src/lib/generated/sitesBrowserLoginRequest.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ - -export interface SitesBrowserLoginRequest { - /** - * Server-generated application-login nonce, distinct from the identity provider SSO session. - * @pattern ^[A-Za-z0-9_-]{43}$ - */ - loginId: string; - /** Absolute application-login expiry in Unix seconds. Binding and minting require a future expiry; revocation also accepts an expired login. The server limits future expiry to its login lifetime. */ - loginExpiresAt: number; -} diff --git a/src/lib/generated/sitesBrowserMintRequest.ts b/src/lib/generated/sitesBrowserMintRequest.ts index adfb9a0..0027dcb 100644 --- a/src/lib/generated/sitesBrowserMintRequest.ts +++ b/src/lib/generated/sitesBrowserMintRequest.ts @@ -5,7 +5,16 @@ * API documentation for lifecycle * OpenAPI spec version: 2.0.0 */ -import type { SitesBrowserLoginRequest } from './sitesBrowserLoginRequest.js'; -import type { SitesBrowserMintRequestAllOf } from './sitesBrowserMintRequestAllOf.js'; -export type SitesBrowserMintRequest = SitesBrowserLoginRequest & SitesBrowserMintRequestAllOf; +export interface SitesBrowserMintRequest { + /** + * Site identifier matching the bootstrap challenge. + * @pattern ^[a-z0-9-]{1,64}$ + */ + siteId: string; + /** + * Opaque, unexpired bootstrap challenge issued by the content gateway. + * @pattern ^[A-Za-z0-9_-]{43}$ + */ + state: string; +} diff --git a/src/lib/generated/sitesBrowserMintRequestAllOf.ts b/src/lib/generated/sitesBrowserMintRequestAllOf.ts deleted file mode 100644 index 5f71221..0000000 --- a/src/lib/generated/sitesBrowserMintRequestAllOf.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated by orval v7.4.1 🍺 - * Do not edit manually. - * Lifecycle API - * API documentation for lifecycle - * OpenAPI spec version: 2.0.0 - */ - -export type SitesBrowserMintRequestAllOf = { - /** - * Opaque, unexpired bootstrap challenge issued by the content gateway. - * @pattern ^[A-Za-z0-9_-]{43}$ - */ - state: string; - /** Site identifier matching the bootstrap challenge. */ - siteId: string; -}; From 8dab9f14b45673703f3244ae220919042f0805bd Mon Sep 17 00:00:00 2001 From: vigneshrajsb Date: Tue, 22 Sep 2026 08:56:13 -0700 Subject: [PATCH 3/6] fix: clarify stable Site URL on visibility change --- src/commands/sites.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/sites.ts b/src/commands/sites.ts index 8bb21c1..48b8715 100644 --- a/src/commands/sites.ts +++ b/src/commands/sites.ts @@ -30,7 +30,7 @@ async function confirmVisibility(visibility: SiteVisibility, yes?: boolean): Pro message: visibility === 'public' ? 'Publish this site so anyone with the link can view it?' - : 'Make this site private? Its current content URL will stop working. Saved copies cannot be recalled.', + : 'Make this site private? Its URL stays the same, but saved copies cannot be recalled.', }); if (ok !== true) { process.stderr.write('Aborted.\n'); From 21b83eff470271003a5d37a51698d510739a669e Mon Sep 17 00:00:00 2001 From: vigneshrajsb Date: Tue, 22 Sep 2026 09:23:36 -0700 Subject: [PATCH 4/6] docs: align Sites CLI help with stable URLs --- README.md | 4 ++-- src/commands/sites.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e1a836a..0839331 100644 --- a/README.md +++ b/README.md @@ -161,12 +161,12 @@ Upload a ZIP, a single HTML file, or a directory (auto-zipped) and get a stable User and personal-key uploads are private by default. Service-key uploads default to public for automation. An explicit unsupported visibility is rejected before upload. Only the owner can replace, extend, delete, or change visibility. Public sites allow anyone with the link to view them. -Use `openUrl` for a stable opening link. Making a public site private retires its old content URL; saved copies cannot be recalled. Named-user sharing is not included in this version. +Use `openUrl` for a stable opening link. Visibility changes keep the Site ID and content URL; saved copies cannot be recalled. Named-user sharing is not included in this version. ```bash lfc sites create ./report.html --name "perf report" # ✓ Created site a1b2c3d4e5 (perf report) -# https://a1b2c3d4e5.sites.lifecycle.example.com +# https://ui.lifecycle.example.com/sites/open/a1b2c3d4e5 lfc sites create ./dist # whole directory lfc sites create ./dist --visibility public --yes diff --git a/src/commands/sites.ts b/src/commands/sites.ts index 48b8715..ed5bec4 100644 --- a/src/commands/sites.ts +++ b/src/commands/sites.ts @@ -226,7 +226,7 @@ export function registerSitesCommands(program: Command): void { ); sites .command('visibility ') - .description('Publish a site or make it private (changes its content URL)') + .description('Publish a site or make it private (keeps its content URL)') .option('-y, --yes', 'skip the confirmation prompt') .action( runAction(async (ctx, siteId: string, value: string, opts: { yes?: boolean }) => { From 4db4c1129c74855afa8f316bd18ec7c15256c4d6 Mon Sep 17 00:00:00 2001 From: vigneshrajsb Date: Tue, 22 Sep 2026 09:24:53 -0700 Subject: [PATCH 5/6] docs: update Sites agent guidance for visibility --- src/commands/llms.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands/llms.ts b/src/commands/llms.ts index ddfab05..6a63dcf 100644 --- a/src/commands/llms.ts +++ b/src/commands/llms.ts @@ -150,10 +150,12 @@ Schema validation (offline, no auth needed): Static sites: - lfc sites create [--name