@clickraft/cli 0.1.5 → 0.1.7

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/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
- ## [0.1.5](https://github.com/clickraft/cli/compare/v0.1.4...v0.1.5) (2026-05-23)
1
+ ## [0.1.7](https://github.com/clickraft/cli/compare/v0.1.6...v0.1.7) (2026-05-23)
2
2
 
3
3
  ### Bug Fixes
4
4
 
5
- * **tokens:** align /tokens list response to { data: T[] } envelope ([72d834d](https://github.com/clickraft/cli/commit/72d834db182b884b740aebc38153e6a0c51db95b))
5
+ * **auth:** include Cloudflare Access headers in OAuth device flow requests ([e2cb7e9](https://github.com/clickraft/cli/commit/e2cb7e966f8935b43142a3d3d87a440e3478db10))
6
6
 
7
7
  # Changelog
8
8
 
package/README.md CHANGED
@@ -21,6 +21,19 @@ clickraft generate create --prompt "a serene mountain landscape at dawn"
21
21
 
22
22
  Full documentation: [docs.clickraft.ai/cli](https://docs.clickraft.ai/cli) _(coming soon)_
23
23
 
24
+ ## Connecting to staging
25
+
26
+ The staging API (`https://staging.clickraft.ai/api/agents/v1`) sits behind Cloudflare Access Zero Trust. To authenticate non-interactively, create a service token in the Cloudflare Zero Trust dashboard (**Access → Service Auth → Service Tokens**), then export both halves before invoking the CLI:
27
+
28
+ ```bash
29
+ export CLOUDFLARE_ACCESS_CLIENT_ID="<client-id>.access"
30
+ export CLOUDFLARE_ACCESS_CLIENT_SECRET="<client-secret>"
31
+
32
+ clickraft tokens list --profile staging
33
+ ```
34
+
35
+ Both env vars must be set together — if only one is set the CLI exits with a configuration error. Production usage (the default) does not require these env vars.
36
+
24
37
  ## Use with AI coding agents
25
38
 
26
39
  The Clickraft CLI is designed to work with Claude Code, Cursor, Codex, and other AI coding agents. Install the skills package for your agent:
package/dist/cli.js CHANGED
@@ -529,6 +529,10 @@ var ApiClient = class {
529
529
  if (this.options.accessToken) {
530
530
  h.authorization = `Bearer ${this.options.accessToken}`;
531
531
  }
532
+ if (this.options.cloudflareAccess) {
533
+ h["cf-access-client-id"] = this.options.cloudflareAccess.clientId;
534
+ h["cf-access-client-secret"] = this.options.cloudflareAccess.clientSecret;
535
+ }
532
536
  if (opts.body !== void 0 && opts.method !== "GET" && opts.method !== "DELETE") {
533
537
  h["content-type"] = "application/json";
534
538
  }
@@ -1173,8 +1177,11 @@ var TOKEN_ENV_VAR = "CLICKRAFT_TOKEN";
1173
1177
  var PROFILE_ENV_VAR = "CLICKRAFT_PROFILE";
1174
1178
  var API_BASE_URL_ENV_VAR = "CLICKRAFT_API_BASE_URL";
1175
1179
  var TELEMETRY_DISABLE_ENV_VAR = "CLICKRAFT_DISABLE_TELEMETRY";
1180
+ var CLOUDFLARE_ACCESS_CLIENT_ID_ENV_VAR = "CLOUDFLARE_ACCESS_CLIENT_ID";
1181
+ var CLOUDFLARE_ACCESS_CLIENT_SECRET_ENV_VAR = "CLOUDFLARE_ACCESS_CLIENT_SECRET";
1176
1182
  async function loadConfig(options = {}) {
1177
1183
  const env = options.env ?? process.env;
1184
+ const cloudflareAccess = loadCloudflareAccess(env);
1178
1185
  const flagToken = trim(options.flagToken);
1179
1186
  const envToken = trim(env[TOKEN_ENV_VAR]);
1180
1187
  const flagProfile = trim(options.flagProfile);
@@ -1204,9 +1211,26 @@ async function loadConfig(options = {}) {
1204
1211
  return {
1205
1212
  accessToken: tokenFromFlagOrEnv ?? fileAccessToken ?? null,
1206
1213
  profileName: profileName ?? fileProfileName ?? DEFAULT_PROFILE_NAME,
1207
- apiBaseUrl: apiBaseFromFlagOrEnv ?? fileApiBaseUrl ?? DEFAULT_API_BASE_URL
1214
+ apiBaseUrl: apiBaseFromFlagOrEnv ?? fileApiBaseUrl ?? DEFAULT_API_BASE_URL,
1215
+ cloudflareAccess
1208
1216
  };
1209
1217
  }
1218
+ function loadCloudflareAccess(env) {
1219
+ const clientId = trim(env[CLOUDFLARE_ACCESS_CLIENT_ID_ENV_VAR]);
1220
+ const clientSecret = trim(env[CLOUDFLARE_ACCESS_CLIENT_SECRET_ENV_VAR]);
1221
+ if (clientId && clientSecret) {
1222
+ return { clientId, clientSecret };
1223
+ }
1224
+ if (!clientId && !clientSecret) {
1225
+ return null;
1226
+ }
1227
+ const onlySet = clientId ? CLOUDFLARE_ACCESS_CLIENT_ID_ENV_VAR : CLOUDFLARE_ACCESS_CLIENT_SECRET_ENV_VAR;
1228
+ throw new ApiError(
1229
+ "INPUT_INVALID_FORMAT",
1230
+ `${CLOUDFLARE_ACCESS_CLIENT_ID_ENV_VAR} and ${CLOUDFLARE_ACCESS_CLIENT_SECRET_ENV_VAR} must both be set, or neither. Got only ${onlySet}.`,
1231
+ { retryable: false }
1232
+ );
1233
+ }
1210
1234
  function trim(value) {
1211
1235
  if (value === void 0) return void 0;
1212
1236
  const t = value.trim();
@@ -1300,7 +1324,8 @@ async function buildClient(opts) {
1300
1324
  return new ApiClient({
1301
1325
  apiBaseUrl: cfg.apiBaseUrl,
1302
1326
  accessToken: cfg.accessToken,
1303
- signal: opts.signal
1327
+ signal: opts.signal,
1328
+ cloudflareAccess: cfg.cloudflareAccess
1304
1329
  });
1305
1330
  }
1306
1331
  async function postAssetMetadata(args) {
@@ -1466,7 +1491,8 @@ async function buildGenerateClient(opts) {
1466
1491
  return new ApiClient({
1467
1492
  apiBaseUrl: cfg.apiBaseUrl,
1468
1493
  accessToken: cfg.accessToken,
1469
- signal: opts.signal
1494
+ signal: opts.signal,
1495
+ cloudflareAccess: cfg.cloudflareAccess
1470
1496
  });
1471
1497
  }
1472
1498
  function isUrlLike(value) {
@@ -1696,7 +1722,8 @@ async function runDeviceFlow(opts) {
1696
1722
  apiBaseUrl: opts.apiBaseUrl,
1697
1723
  clientId: opts.clientId,
1698
1724
  deviceCode: authResp.device_code,
1699
- signal: opts.signal
1725
+ signal: opts.signal,
1726
+ cloudflareAccess: opts.cloudflareAccess
1700
1727
  });
1701
1728
  if (result.kind === "success") {
1702
1729
  return result.token;
@@ -1731,11 +1758,16 @@ async function runDeviceFlow(opts) {
1731
1758
  }
1732
1759
  async function requestDeviceAuthorization(opts) {
1733
1760
  const body = new URLSearchParams({ client_id: opts.clientId, scope: opts.scope }).toString();
1761
+ const headers = { "content-type": FORM_CONTENT_TYPE };
1762
+ if (opts.cloudflareAccess) {
1763
+ headers["cf-access-client-id"] = opts.cloudflareAccess.clientId;
1764
+ headers["cf-access-client-secret"] = opts.cloudflareAccess.clientSecret;
1765
+ }
1734
1766
  let resp;
1735
1767
  try {
1736
1768
  resp = await request(`${opts.apiBaseUrl}/oauth/device-authorization`, {
1737
1769
  method: "POST",
1738
- headers: { "content-type": FORM_CONTENT_TYPE },
1770
+ headers,
1739
1771
  body,
1740
1772
  signal: opts.signal
1741
1773
  });
@@ -1802,11 +1834,16 @@ async function pollToken(args) {
1802
1834
  device_code: args.deviceCode,
1803
1835
  grant_type: GRANT_TYPE
1804
1836
  }).toString();
1837
+ const headers = { "content-type": FORM_CONTENT_TYPE };
1838
+ if (args.cloudflareAccess) {
1839
+ headers["cf-access-client-id"] = args.cloudflareAccess.clientId;
1840
+ headers["cf-access-client-secret"] = args.cloudflareAccess.clientSecret;
1841
+ }
1805
1842
  let resp;
1806
1843
  try {
1807
1844
  resp = await request(`${args.apiBaseUrl}/oauth/token`, {
1808
1845
  method: "POST",
1809
- headers: { "content-type": FORM_CONTENT_TYPE },
1846
+ headers,
1810
1847
  body,
1811
1848
  signal: args.signal
1812
1849
  });
@@ -2010,7 +2047,8 @@ async function login(options) {
2010
2047
  scope,
2011
2048
  onUserCode: options.onUserCode,
2012
2049
  onPoll: options.onPoll,
2013
- signal: options.signal
2050
+ signal: options.signal,
2051
+ cloudflareAccess: config.cloudflareAccess
2014
2052
  });
2015
2053
  const profile = buildCredentialsProfile({ token, apiBaseUrl });
2016
2054
  await setProfile(profileName, profile, options.credentialsPath);
@@ -2073,7 +2111,12 @@ async function logout(options) {
2073
2111
  let serverRevoked = false;
2074
2112
  let revokeFailureMessage;
2075
2113
  if (options.revoke) {
2076
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({ apiBaseUrl, accessToken, signal: options.signal }));
2114
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
2115
+ apiBaseUrl,
2116
+ accessToken,
2117
+ signal: options.signal,
2118
+ cloudflareAccess: config.cloudflareAccess
2119
+ }));
2077
2120
  const client = factory(profile.apiBaseUrl, profile.accessToken);
2078
2121
  try {
2079
2122
  await client.request({
@@ -2198,7 +2241,12 @@ async function listTokens(options) {
2198
2241
  if (!config.accessToken) {
2199
2242
  throw new AuthError("AUTH_TOKEN_MISSING", `Not authenticated. Run \`clickraft login\` first.`);
2200
2243
  }
2201
- const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({ apiBaseUrl, accessToken, signal: options.signal }));
2244
+ const factory = options.apiClientFactory ?? ((apiBaseUrl, accessToken) => new ApiClient({
2245
+ apiBaseUrl,
2246
+ accessToken,
2247
+ signal: options.signal,
2248
+ cloudflareAccess: config.cloudflareAccess
2249
+ }));
2202
2250
  const client = factory(config.apiBaseUrl, config.accessToken);
2203
2251
  const data = await client.request({
2204
2252
  method: "GET",