@camox/cli 0.43.0 → 0.45.0
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/README.md +144 -40
- package/dist/api-DNIQC1ea.mjs +104 -0
- package/dist/api-DeE66CPa.mjs +2 -0
- package/dist/auth-core-xLeorUUV.mjs +120 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +673 -705
- package/dist/lib/auth-core.d.mts +12 -8
- package/dist/lib/auth-core.mjs +1 -2
- package/dist/local-dns-DQIT1ucr.mjs +22 -0
- package/package.json +23 -19
- package/dist/api-COfB7AKI.mjs +0 -82
- package/dist/api-LuNKejJE.mjs +0 -3
- package/dist/auth-core-cCuk5aZy.mjs +0 -63
- package/templates/default/.env.example +0 -2
- package/templates/default/components.json +0 -25
- package/templates/default/package.json +0 -46
- package/templates/default/scripts/README.md +0 -35
- package/templates/default/scripts/production-smoke.mjs +0 -124
- package/templates/default/src/blocks/faq.tsx +0 -63
- package/templates/default/src/blocks/footer.tsx +0 -61
- package/templates/default/src/blocks/hero.tsx +0 -92
- package/templates/default/src/blocks/navbar.tsx +0 -110
- package/templates/default/src/blocks/youtube-video.tsx +0 -158
- package/templates/default/src/components/ui/accordion.tsx +0 -70
- package/templates/default/src/components/ui/button.tsx +0 -58
- package/templates/default/src/document.ts +0 -7
- package/templates/default/src/layouts/default.tsx +0 -88
- package/templates/default/src/lib/utils.ts +0 -6
- package/templates/default/src/styles.css +0 -162
- package/templates/default/tsconfig.json +0 -25
- package/templates/default/vite.config.ts +0 -25
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Runtime lookup and monorepos
|
|
4
4
|
|
|
5
|
-
Runtime-backed commands (`status`, `pages`, `blocks`, `layouts`, `
|
|
6
|
-
and `logout`) accept `--cwd PATH`:
|
|
5
|
+
Runtime-backed commands (`status`, `pages`, `blocks`, `layouts`, `locales`, `files`,
|
|
6
|
+
`publish`, `discard`, `preview`, and `logout`) accept `--cwd PATH`:
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
9
|
camox pages list --cwd apps/site
|
|
@@ -19,38 +19,139 @@ build once to generate its runtime file.
|
|
|
19
19
|
Relative `--cwd` paths resolve against the shell's current directory. This option
|
|
20
20
|
only affects runtime lookup: other paths, such as `--file ./hero.webp`, still
|
|
21
21
|
resolve against the shell's current directory. Missing or non-directory paths fail
|
|
22
|
-
instead of falling back to the shell's directory.
|
|
23
|
-
|
|
22
|
+
instead of falling back to the shell's directory.
|
|
23
|
+
|
|
24
|
+
## Your development draft
|
|
25
|
+
|
|
26
|
+
Every command reads and writes your development draft: it shows Live wherever
|
|
27
|
+
it changes nothing, and publishing it lands on Live. No command takes a draft
|
|
28
|
+
flag. Dev mode creates the draft on its first sync after `camox login`, so run
|
|
29
|
+
your site locally once (e.g. `pnpm dev`) before using the CLI; until then,
|
|
30
|
+
commands fail with `NO_DEVELOPMENT_DRAFT`.
|
|
31
|
+
|
|
32
|
+
Agents in parallel worktrees share their developer's development draft, and
|
|
33
|
+
publish or discard it together.
|
|
34
|
+
|
|
35
|
+
## Publishing and discarding
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
camox status # whether your development draft exists, and how many changes it holds
|
|
39
|
+
camox publish # what publishing would do, with a token
|
|
40
|
+
camox publish --confirm TOKEN # publish to Live
|
|
41
|
+
camox discard # empty your development draft (asks first; --yes skips the question)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`camox publish` lists, in order:
|
|
45
|
+
|
|
46
|
+
- **Blockers**, which prevent publishing until they're fixed in the draft. They
|
|
47
|
+
include definitions that differ from Live's: your local code changed a block,
|
|
48
|
+
layout or collection, and only a release (`camox release`) can ship it to Live.
|
|
49
|
+
- **Conflicts**: content Live changed or deleted since your draft changed it,
|
|
50
|
+
with who published it and when. Publishing overwrites Live's version.
|
|
51
|
+
- **Lost changes**, which publishing drops because what they belong to is gone.
|
|
52
|
+
- **Warnings**: the same problems already on Live, which don't block.
|
|
53
|
+
- **Changes**: everything your draft creates, changes or deletes.
|
|
54
|
+
|
|
55
|
+
The token records the conflicts the list showed. `camox publish --confirm TOKEN`
|
|
56
|
+
publishes the whole draft, which then stays open, emptied. If Live changed since
|
|
57
|
+
the list was printed, nothing is published: the command prints the new list and
|
|
58
|
+
token, fails with `CONFLICTS_CHANGED`, and you review it before confirming again.
|
|
59
|
+
|
|
60
|
+
`camox discard` empties your development draft without touching Live. It asks
|
|
61
|
+
for confirmation in a terminal; elsewhere it fails with `CONFIRMATION_REQUIRED`
|
|
62
|
+
unless you pass `--yes`.
|
|
63
|
+
|
|
64
|
+
## Authenticated local preview
|
|
65
|
+
|
|
66
|
+
CLI login does not sign in a fresh browser. To verify writes in your development draft:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
camox preview --url http://localhost:3000/about --json
|
|
70
|
+
camox preview --cwd apps/site --url http://localhost:3000/about --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Open the returned `url` in the browser you use for verification. Wait for
|
|
74
|
+
`[data-camox-preview="ready"]`, which identifies the project and your
|
|
75
|
+
development draft after authenticated rendering. Errors appear as
|
|
76
|
+
`[data-camox-preview="error"]`; expired or consumed links require a fresh command.
|
|
77
|
+
The CLI and SDK must both support the preview handoff.
|
|
78
|
+
|
|
79
|
+
The URL transfers your signed-in session: treat it as a credential, do not share
|
|
80
|
+
or commit it, and use it within three minutes. Only HTTP(S) loopback URLs are
|
|
81
|
+
accepted. The destination must match the CLI's project and backend. There is no
|
|
82
|
+
automatic default-browser opening.
|
|
83
|
+
The local dev server must already be running.
|
|
24
84
|
|
|
25
85
|
## Page SEO
|
|
26
86
|
|
|
27
|
-
Use `pages get` to read metadata and `pages
|
|
87
|
+
Use `pages get` to read metadata and `pages set-seo` to edit it:
|
|
28
88
|
|
|
29
89
|
```sh
|
|
30
90
|
camox pages get --path /about --json
|
|
31
|
-
camox pages
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
camox pages
|
|
35
|
-
camox pages
|
|
36
|
-
camox pages
|
|
91
|
+
camox pages set-seo --path /about --meta-title "About us" --meta-description "Meet the team."
|
|
92
|
+
# Page and block IDs are UUIDv7s; anything else is rejected.
|
|
93
|
+
PAGE_ID=0199f1c2-7b3e-7a10-9c4d-5e6f7a8b9c0a
|
|
94
|
+
camox pages set-seo --id "$PAGE_ID" --meta-description ""
|
|
95
|
+
camox pages set-seo --id "$PAGE_ID" --ai-seo on
|
|
96
|
+
camox pages set-seo --id "$PAGE_ID" --ai-seo off
|
|
37
97
|
```
|
|
38
98
|
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
99
|
+
- `set-seo` requires exactly one of `--id` or `--path` and at least one of
|
|
100
|
+
`--meta-title`, `--meta-description` or `--ai-seo`. An empty meta title blocks
|
|
101
|
+
publishing.
|
|
102
|
+
- The meta title and description are one group: both generated or both manual.
|
|
103
|
+
Writing either field makes **both** manual, keeping the current value of the
|
|
104
|
+
other. Manual metadata cannot be combined with `--ai-seo on`.
|
|
105
|
+
- `--ai-seo off` keeps current metadata as manual. `--ai-seo on` schedules asynchronous
|
|
44
106
|
generation; the returned page contains current values, not necessarily generated
|
|
45
107
|
values. Use `pages get` to inspect them later.
|
|
46
|
-
- Changes
|
|
47
|
-
|
|
48
|
-
- Commands support `--project
|
|
49
|
-
|
|
50
|
-
selects the environment; it does not publish the draft.
|
|
108
|
+
- Changes are visible in your development draft right away, and reach Live
|
|
109
|
+
when you publish it.
|
|
110
|
+
- Commands support `--project PROJECT` (a Project ID or Project Slug) and
|
|
111
|
+
`--json`, with structured JSON output/errors.
|
|
51
112
|
|
|
52
113
|
See `camox pages update --help` for options and examples.
|
|
53
114
|
|
|
115
|
+
## Shared navigation and footer content
|
|
116
|
+
|
|
117
|
+
`pages get` shows page-owned blocks. To find layout-owned blocks:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
camox layouts list
|
|
121
|
+
camox layouts get --layout-id default --json
|
|
122
|
+
BLOCK_ID=0199f1c2-7b3e-7a10-9c4d-5e6f7a8b9c0b
|
|
123
|
+
camox blocks get --id "$BLOCK_ID"
|
|
124
|
+
camox blocks edit --id "$BLOCK_ID" --content '{"title": "Updated title"}'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A layout is identified by the name its code gives it: the `layoutId` from
|
|
128
|
+
`layouts list`, which is also a page's `layoutId`. `layouts get` returns
|
|
129
|
+
`blocks` with IDs, types, layout slot, and content, along with ordered
|
|
130
|
+
`layout.beforeBlockIds` and `layout.afterBlockIds`. Use a returned block `id`
|
|
131
|
+
with `blocks get` and `blocks edit`, inspecting its schema and current content
|
|
132
|
+
before editing.
|
|
133
|
+
|
|
134
|
+
Commands accept `--cwd`, `--project`, and `--json`.
|
|
135
|
+
Keep the project consistent when using discovered IDs.
|
|
136
|
+
Editing a shared layout block changes it for all pages using that layout.
|
|
137
|
+
|
|
138
|
+
## Locales
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
camox locales list
|
|
142
|
+
camox locales add --tag de
|
|
143
|
+
camox pages get --path /pricing --locale de
|
|
144
|
+
camox locales remove --tag de
|
|
145
|
+
camox locales set-default-tag --tag en-GB
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Locales are added and removed in your development draft, like content. Camox
|
|
149
|
+
translates the default locale into every other locale of the draft, when a
|
|
150
|
+
locale is added and after every change to the default locale. Content commands
|
|
151
|
+
(`pages`, `blocks`, `layouts get`) take `--locale <TAG>` and act on the default
|
|
152
|
+
locale without it. Correcting the default locale's tag applies at once
|
|
153
|
+
and is only possible while it's the only locale.
|
|
154
|
+
|
|
54
155
|
## Media
|
|
55
156
|
|
|
56
157
|
Run `camox login` and start your project's dev server (or build once) so the CLI
|
|
@@ -67,24 +168,27 @@ camox files upload --url https://example.com/hero.webp --alt "Product screenshot
|
|
|
67
168
|
camox files upload --url https://example.com/download --filename hero.webp --ai-metadata off
|
|
68
169
|
|
|
69
170
|
# Inspect the returned ID and URL; use the ID in your block's media field.
|
|
171
|
+
# File IDs are UUIDv7s; anything else is rejected.
|
|
70
172
|
camox files list
|
|
71
|
-
|
|
173
|
+
FILE_ID=0199f1c2-7b3e-7a10-9c4d-5e6f7a8b9c0d
|
|
174
|
+
camox files get --id "$FILE_ID"
|
|
72
175
|
|
|
73
|
-
camox files update --id
|
|
74
|
-
camox files update --id
|
|
75
|
-
camox files update --id
|
|
76
|
-
camox files update --id
|
|
176
|
+
camox files update --id "$FILE_ID" --filename hero.webp
|
|
177
|
+
camox files update --id "$FILE_ID" --alt "Updated description"
|
|
178
|
+
camox files update --id "$FILE_ID" --alt ""
|
|
179
|
+
camox files update --id "$FILE_ID" --ai-metadata on
|
|
77
180
|
|
|
78
181
|
# Replace the underlying file everywhere it is referenced, preserving its ID.
|
|
79
|
-
camox files update --id
|
|
80
|
-
camox files update --id
|
|
182
|
+
camox files update --id "$FILE_ID" --file ./new-hero.webp
|
|
183
|
+
camox files update --id "$FILE_ID" --url https://example.com/new-hero.webp
|
|
81
184
|
|
|
82
185
|
# Replace content and override metadata together.
|
|
83
|
-
camox files update --id
|
|
186
|
+
camox files update --id "$FILE_ID" --file ./new-hero.webp --filename hero.webp --alt "New screenshot"
|
|
84
187
|
```
|
|
85
188
|
|
|
86
189
|
- Upload requires exactly one of `--file` or `--url`. URLs are copied, not linked.
|
|
87
|
-
-
|
|
190
|
+
- Alt text and filename are one group: both generated or both manual. `--alt`
|
|
191
|
+
(even an empty string) makes both manual, keeping the current filename.
|
|
88
192
|
It cannot be combined with `--ai-metadata on`. Manual edits are protected from
|
|
89
193
|
already-running AI jobs.
|
|
90
194
|
- Without metadata flags, raster-image uploads use automatic metadata. Other
|
|
@@ -93,26 +197,26 @@ camox files update --id 123 --file ./new-hero.webp --filename hero.webp --alt "N
|
|
|
93
197
|
generation has completed. Use `files get` to inspect the current record.
|
|
94
198
|
- Update requires at least one of `--file`, `--url`, `--filename`, `--alt` or
|
|
95
199
|
`--ai-metadata`. `--file` and `--url` are mutually exclusive; without either,
|
|
96
|
-
only metadata changes. Renaming alone preserves the file ID
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
200
|
+
only metadata changes. Renaming alone preserves the file ID and URL and makes
|
|
201
|
+
both alt text and filename manual.
|
|
202
|
+
- Replacing content preserves the file ID and all `_fileId` references in your
|
|
203
|
+
development draft. The URL changes to avoid stale caches. Direct URLs in
|
|
204
|
+
stored block/repeatable-item content are migrated within that draft;
|
|
100
205
|
URLs hardcoded outside Camox cannot be rewritten.
|
|
101
206
|
- Replacements preserve omitted alt text and AI settings. The filename defaults
|
|
102
207
|
to the new source filename; `--filename` overrides it. Binary metadata and
|
|
103
208
|
explicit metadata changes are saved together. If AI remains enabled, raster
|
|
104
209
|
replacements schedule generation on the original file ID; other types skip
|
|
105
210
|
generation. Disable AI to prevent future automatic renaming or alt changes.
|
|
106
|
-
- Files are shared assets
|
|
107
|
-
to it in
|
|
211
|
+
- Files are shared assets: replacing one affects every reference
|
|
212
|
+
to it in your development draft.
|
|
108
213
|
- Failed replacements leave the original file unchanged and remove the unused
|
|
109
214
|
uploaded binary. After a network timeout, use `files get --id ID` before retrying
|
|
110
215
|
because the server may already have completed the operation.
|
|
111
|
-
- All commands support `--project
|
|
112
|
-
|
|
113
|
-
project and environment.
|
|
216
|
+
- All commands support `--project PROJECT` (a Project ID or Project Slug) and
|
|
217
|
+
`--json`. IDs are scoped to the selected project.
|
|
114
218
|
- Uploads return the stored file record, including `id`, `url`, `mimeType`, `alt`,
|
|
115
|
-
and `
|
|
219
|
+
and `aiMetadata`. Output is JSON; errors are structured JSON on stderr.
|
|
116
220
|
- Files are limited to 100 MiB. URL downloads allow at most five redirects and
|
|
117
221
|
have a two-minute timeout; uploads have a separate two-minute timeout.
|
|
118
222
|
Downloads stream through a temporary file that is removed afterward.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as installLocalhostDns } from "./local-dns-DQIT1ucr.mjs";
|
|
3
|
+
import { createORPCClient } from "@orpc/client";
|
|
4
|
+
import { RPCLink } from "@orpc/client/fetch";
|
|
5
|
+
//#region ../api-contract/src/drafts.ts
|
|
6
|
+
/** Request header naming the draft a request reads and writes (ADR 0011). Absent means Live. */
|
|
7
|
+
const DRAFT_HEADER = "x-camox-draft";
|
|
8
|
+
/**
|
|
9
|
+
* The `x-camox-draft` value naming the caller's own development draft in the project a request
|
|
10
|
+
* acts on (ADR 0006, ADR 0011). Dev mode and the CLI always send it.
|
|
11
|
+
*/
|
|
12
|
+
const DEVELOPMENT_DRAFT = "development";
|
|
13
|
+
/**
|
|
14
|
+
* oRPC error code for a request naming the development draft before dev mode created it: the
|
|
15
|
+
* first dev-mode sync after login does, so the site has to run locally once first.
|
|
16
|
+
*/
|
|
17
|
+
const NO_DEVELOPMENT_DRAFT_ERROR = "NO_DEVELOPMENT_DRAFT";
|
|
18
|
+
/**
|
|
19
|
+
* The oRPC error code a request naming an ended draft fails with. Its `data` is an `EndedDraft`,
|
|
20
|
+
* so a page can render Live and say who published or discarded the draft.
|
|
21
|
+
*/
|
|
22
|
+
const DRAFT_ENDED_ERROR_CODE = "DRAFT_ENDED";
|
|
23
|
+
/**
|
|
24
|
+
* Why the publish tool published nothing of a draft with changes, and returned a fresh preview to
|
|
25
|
+
* review again, by the error code of each: Live changed since the preview, the draft has blockers,
|
|
26
|
+
* or translations are still running.
|
|
27
|
+
*/
|
|
28
|
+
const NOT_PUBLISHED_CODES = {
|
|
29
|
+
conflictsChanged: "CONFLICTS_CHANGED",
|
|
30
|
+
blocked: "PUBLISH_BLOCKED",
|
|
31
|
+
translating: "TRANSLATIONS_PENDING"
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/lib/api.ts
|
|
35
|
+
const CAMOX_API_URL = process.env.CAMOX_API_URL || "https://api.camox.dev";
|
|
36
|
+
installLocalhostDns(new URL(CAMOX_API_URL).hostname);
|
|
37
|
+
function authHeaders(token) {
|
|
38
|
+
return {
|
|
39
|
+
Authorization: `Bearer ${token}`,
|
|
40
|
+
"Content-Type": "application/json"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Headers of every request a command sends about a project: the CLI works only in the
|
|
45
|
+
* caller's development draft (ADR 0010).
|
|
46
|
+
*/
|
|
47
|
+
function developmentDraftHeaders(token, disableTelemetry) {
|
|
48
|
+
const headers = {
|
|
49
|
+
Authorization: `Bearer ${token}`,
|
|
50
|
+
"x-camox-client": "cli",
|
|
51
|
+
[DRAFT_HEADER]: DEVELOPMENT_DRAFT
|
|
52
|
+
};
|
|
53
|
+
if (disableTelemetry) headers["x-camox-telemetry-disabled"] = "1";
|
|
54
|
+
return headers;
|
|
55
|
+
}
|
|
56
|
+
function createRpcClient(token, apiUrl = CAMOX_API_URL, options = {}) {
|
|
57
|
+
installLocalhostDns(new URL(apiUrl).hostname);
|
|
58
|
+
const headers = options.developmentDraft ? developmentDraftHeaders(token, options.disableTelemetry) : {
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
"x-camox-client": "cli"
|
|
61
|
+
};
|
|
62
|
+
const link = new RPCLink({
|
|
63
|
+
url: `${apiUrl}/rpc`,
|
|
64
|
+
headers
|
|
65
|
+
});
|
|
66
|
+
return createORPCClient(link);
|
|
67
|
+
}
|
|
68
|
+
async function verifySession(token) {
|
|
69
|
+
return (await fetch(`${CAMOX_API_URL}/api/auth/get-session`, {
|
|
70
|
+
method: "GET",
|
|
71
|
+
headers: authHeaders(token)
|
|
72
|
+
})).ok;
|
|
73
|
+
}
|
|
74
|
+
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
75
|
+
/**
|
|
76
|
+
* Load a project named by its Project ID or its Project Slug. A UUID-shaped name is a
|
|
77
|
+
* Project ID, which the API validates; anything else is a slug.
|
|
78
|
+
*/
|
|
79
|
+
async function getProject(token, project, apiUrl) {
|
|
80
|
+
const client = createRpcClient(token, apiUrl, { developmentDraft: true });
|
|
81
|
+
return UUID_SHAPE.test(project) ? client.projects.getByUuid({ projectUuid: project }) : client.projects.getBySlug({ slug: project });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The caller's development draft in a project, summarized: it fails with
|
|
85
|
+
* `NO_DEVELOPMENT_DRAFT` until dev mode created it.
|
|
86
|
+
*/
|
|
87
|
+
async function getDevelopmentDraft(params) {
|
|
88
|
+
return createRpcClient(params.token, params.apiUrl, {
|
|
89
|
+
developmentDraft: true,
|
|
90
|
+
disableTelemetry: params.disableTelemetry
|
|
91
|
+
}).drafts.get({ projectId: params.projectId });
|
|
92
|
+
}
|
|
93
|
+
async function callTool(params) {
|
|
94
|
+
return await createRpcClient(params.token, params.apiUrl, {
|
|
95
|
+
developmentDraft: true,
|
|
96
|
+
disableTelemetry: params.disableTelemetry
|
|
97
|
+
}).agent.callTool({
|
|
98
|
+
projectId: params.projectId,
|
|
99
|
+
name: params.name,
|
|
100
|
+
arguments: params.args
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { verifySession as a, NOT_PUBLISHED_CODES as c, getProject as i, NO_DEVELOPMENT_DRAFT_ERROR as l, developmentDraftHeaders as n, DEVELOPMENT_DRAFT as o, getDevelopmentDraft as r, DRAFT_ENDED_ERROR_CODE as s, callTool as t };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as installLocalhostDns } from "./local-dns-DQIT1ucr.mjs";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region src/lib/auth-core.ts
|
|
7
|
+
function globalAuthFile() {
|
|
8
|
+
return path.join(os.homedir(), ".camox", "auth.json");
|
|
9
|
+
}
|
|
10
|
+
function localAuthFile(cwd) {
|
|
11
|
+
let directory = path.resolve(cwd);
|
|
12
|
+
while (true) {
|
|
13
|
+
if (fs.existsSync(path.join(directory, ".git"))) {
|
|
14
|
+
const file = path.join(directory, ".camox", "auth.json");
|
|
15
|
+
return fs.existsSync(file) ? file : null;
|
|
16
|
+
}
|
|
17
|
+
const parent = path.dirname(directory);
|
|
18
|
+
if (parent === directory) return null;
|
|
19
|
+
directory = parent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function normalizeUrl(url) {
|
|
23
|
+
return url.replace(/\/+$/, "");
|
|
24
|
+
}
|
|
25
|
+
function isLoopbackUrl(url) {
|
|
26
|
+
return ["http:", "https:"].includes(url.protocol) && ([
|
|
27
|
+
"localhost",
|
|
28
|
+
"127.0.0.1",
|
|
29
|
+
"[::1]"
|
|
30
|
+
].includes(url.hostname) || url.hostname.endsWith(".localhost")) && !url.username && !url.password;
|
|
31
|
+
}
|
|
32
|
+
async function createPreviewSignInUrl(destination, target, authToken, signal = AbortSignal.timeout(15e3)) {
|
|
33
|
+
const url = new URL(destination);
|
|
34
|
+
if (!isLoopbackUrl(url)) throw new Error("Preview requires an http(s) loopback URL without credentials.");
|
|
35
|
+
installLocalhostDns(new URL(target.apiUrl).hostname);
|
|
36
|
+
url.searchParams.set("camox-preview", JSON.stringify({
|
|
37
|
+
...target,
|
|
38
|
+
apiUrl: normalizeUrl(target.apiUrl)
|
|
39
|
+
}));
|
|
40
|
+
const response = await fetch(`${normalizeUrl(target.apiUrl)}/api/auth/cross-domain/studio-authorize`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${authToken}`,
|
|
44
|
+
"Content-Type": "application/json"
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ callback: url.href }),
|
|
47
|
+
redirect: "error",
|
|
48
|
+
signal
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) throw new Error(`Could not create preview sign-in (${response.status}). Check your session with camox login.`);
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
const token = typeof data.url === "string" ? URL.parse(data.url)?.searchParams.get("ott") : null;
|
|
53
|
+
if (!token) throw new Error("Authentication backend returned no one-time token.");
|
|
54
|
+
url.searchParams.set("ott", token);
|
|
55
|
+
return url.href;
|
|
56
|
+
}
|
|
57
|
+
function readAllTokens(file) {
|
|
58
|
+
if (!fs.existsSync(file)) return {};
|
|
59
|
+
const entries = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
60
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) throw new Error(`Invalid Camox credentials file: ${file}`);
|
|
61
|
+
return entries;
|
|
62
|
+
}
|
|
63
|
+
function writeAllTokens(file, tokens) {
|
|
64
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
65
|
+
fs.writeFileSync(file, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
66
|
+
fs.chmodSync(file, 384);
|
|
67
|
+
}
|
|
68
|
+
function credentialFile(authenticationUrl, cwd) {
|
|
69
|
+
const local = localAuthFile(cwd);
|
|
70
|
+
if (local && Object.hasOwn(readAllTokens(local), normalizeUrl(authenticationUrl))) return local;
|
|
71
|
+
return globalAuthFile();
|
|
72
|
+
}
|
|
73
|
+
/** Look up stored credentials for a specific Camox authentication backend. */
|
|
74
|
+
function readAuthTokenForUrl(authenticationUrl, cwd = process.cwd()) {
|
|
75
|
+
const entry = readAllTokens(credentialFile(authenticationUrl, cwd))[normalizeUrl(authenticationUrl)];
|
|
76
|
+
if (entry?.token && entry?.name && typeof entry.email === "string") return entry;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function writeAuthTokenForUrl(authenticationUrl, token, cwd = process.cwd()) {
|
|
80
|
+
const file = credentialFile(authenticationUrl, cwd);
|
|
81
|
+
const tokens = readAllTokens(file);
|
|
82
|
+
tokens[normalizeUrl(authenticationUrl)] = token;
|
|
83
|
+
writeAllTokens(file, tokens);
|
|
84
|
+
}
|
|
85
|
+
function removeAuthTokenForUrl(authenticationUrl, cwd = process.cwd()) {
|
|
86
|
+
const file = credentialFile(authenticationUrl, cwd);
|
|
87
|
+
const tokens = readAllTokens(file);
|
|
88
|
+
if (file !== globalAuthFile()) {
|
|
89
|
+
tokens[normalizeUrl(authenticationUrl)] = null;
|
|
90
|
+
writeAllTokens(file, tokens);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
delete tokens[normalizeUrl(authenticationUrl)];
|
|
94
|
+
if (Object.keys(tokens).length > 0) {
|
|
95
|
+
writeAllTokens(file, tokens);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
fs.rmSync(file, { force: true });
|
|
99
|
+
}
|
|
100
|
+
async function verifyOneTimeToken(apiUrl, token) {
|
|
101
|
+
installLocalhostDns(new URL(apiUrl).hostname);
|
|
102
|
+
const res = await fetch(`${normalizeUrl(apiUrl)}/api/auth/one-time-token/verify`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ token })
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) throw new Error(`OTT verification failed: ${res.status}`);
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
const user = data.user;
|
|
110
|
+
if (!user?.name) throw new Error("No user info in verification response");
|
|
111
|
+
const sessionToken = data.session?.token;
|
|
112
|
+
if (!sessionToken) throw new Error("No session token in verification response");
|
|
113
|
+
return {
|
|
114
|
+
name: user.name,
|
|
115
|
+
email: user.email ?? "",
|
|
116
|
+
token: sessionToken
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { removeAuthTokenForUrl as a, readAuthTokenForUrl as i, isLoopbackUrl as n, verifyOneTimeToken as o, normalizeUrl as r, writeAuthTokenForUrl as s, createPreviewSignInUrl as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
|
|
2
|
-
export {
|
|
2
|
+
export {}
|