impel-cli 0.14.2 → 0.14.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "Configure Claude Code and Codex CLI to talk to Impel's gateway, authenticated by an Impel Personal Access Token",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@ const SAFE_NAMESPACE = /^[a-z][a-z0-9_-]{1,31}$/u;
11
11
  const SAFE_PREFIX = /^[a-z][a-z0-9_]{2,31}_$/u;
12
12
  const SAFE_ENVIRONMENT_PREFIX = /^[A-Z][A-Z0-9_]{1,31}$/u;
13
13
  const SAFE_ROUTE = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/u;
14
+ const SAFE_TENANT_ID = /^[A-Za-z0-9_.-]{1,128}$/u;
14
15
  const ANSI_ESCAPE_RE = /\u001B(?:\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/gu;
15
16
  const CONTROL_TEST_RE = /[\u0000-\u001F\u007F-\u009F]/u;
16
17
  const CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/gu;
@@ -29,6 +30,15 @@ function validateRoute(name, value) {
29
30
  return value;
30
31
  }
31
32
 
33
+ function validateTenantID(value) {
34
+ if (value == null) return null;
35
+ const tenantID = String(value || "").trim();
36
+ if (!SAFE_TENANT_ID.test(tenantID) || tenantID === "." || tenantID === ".." || CONTROL_TEST_RE.test(tenantID)) {
37
+ throw new Error("gateway CLI tenant.defaultId is invalid");
38
+ }
39
+ return tenantID;
40
+ }
41
+
32
42
  function validateBrand(input) {
33
43
  if (!input || Array.isArray(input) || typeof input !== "object" || input.schemaVersion !== 1) {
34
44
  throw new Error("gateway CLI brand schemaVersion must be 1");
@@ -40,6 +50,8 @@ function validateBrand(input) {
40
50
  const managedMarker = String(input.cli?.managedMarker || "");
41
51
  const environmentPrefix = String(input.cli?.environmentPrefix || command.toUpperCase().replace(/-/gu, "_"));
42
52
  const patPrefix = String(input.auth?.patPrefix || "");
53
+ const tenantPrefix = input.auth?.tenantPrefix == null ? null : String(input.auth.tenantPrefix);
54
+ const defaultTenantID = validateTenantID(input.tenant?.defaultId);
43
55
  if (!SAFE_ID.test(productID)) throw new Error("gateway CLI product.id is invalid");
44
56
  if (!SAFE_ID.test(command)) throw new Error("gateway CLI command is invalid");
45
57
  if (!SAFE_NAMESPACE.test(configNamespace)) throw new Error("gateway CLI configNamespace is invalid");
@@ -47,13 +59,16 @@ function validateBrand(input) {
47
59
  if (!SAFE_NAMESPACE.test(managedMarker)) throw new Error("gateway CLI managedMarker is invalid");
48
60
  if (!SAFE_ENVIRONMENT_PREFIX.test(environmentPrefix)) throw new Error("gateway CLI environmentPrefix is invalid");
49
61
  if (!SAFE_PREFIX.test(patPrefix)) throw new Error("gateway CLI PAT prefix is invalid");
62
+ if (tenantPrefix != null && !SAFE_PREFIX.test(tenantPrefix)) throw new Error("gateway CLI tenant prefix is invalid");
63
+ if (defaultTenantID && !tenantPrefix) throw new Error("gateway CLI tenant prefix is required when tenant.defaultId is set");
50
64
 
51
65
  const defaultOrigin = input.gateway?.defaultOrigin == null ? null : normalizeGatewayUrl(input.gateway.defaultOrigin);
52
66
  return Object.freeze({
53
67
  schemaVersion: 1,
54
68
  product: Object.freeze({ id: productID, displayName: validateText("product.displayName", input.product?.displayName) }),
55
69
  cli: Object.freeze({ command, configNamespace, providerId: providerID, managedMarker, environmentPrefix }),
56
- auth: Object.freeze({ patPrefix }),
70
+ auth: Object.freeze({ patPrefix, tenantPrefix }),
71
+ tenant: Object.freeze({ defaultId: defaultTenantID }),
57
72
  gateway: Object.freeze({
58
73
  defaultOrigin,
59
74
  healthPath: validateRoute("healthPath", input.gateway?.healthPath),
@@ -183,9 +198,21 @@ export function createGatewayCli(options) {
183
198
  return `${secret.slice(0, brand.auth.patPrefix.length + 4)}...${secret.slice(-4)}`;
184
199
  }
185
200
 
201
+ function bearerCredential(pat) {
202
+ if (!brand.tenant.defaultId) return pat;
203
+ const encodedTenant = Buffer.from(brand.tenant.defaultId, "utf8").toString("base64url");
204
+ return `${brand.auth.tenantPrefix}${encodedTenant}.${pat}`;
205
+ }
206
+
186
207
  function redactSecretText(value) {
187
208
  const escapedPrefix = brand.auth.patPrefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
188
- const tokenPattern = new RegExp(`${escapedPrefix}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?`, "gu");
209
+ const escapedTenantPrefix = brand.auth.tenantPrefix?.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
210
+ const tokenPattern = new RegExp(
211
+ escapedTenantPrefix
212
+ ? `(?:${escapedTenantPrefix}[A-Za-z0-9_-]+\\.)?${escapedPrefix}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?`
213
+ : `${escapedPrefix}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?`,
214
+ "gu",
215
+ );
189
216
  return String(value ?? "")
190
217
  .replace(tokenPattern, "[REDACTED GATEWAY CREDENTIAL]")
191
218
  .replace(ANSI_ESCAPE_RE, "")
@@ -244,7 +271,7 @@ export function createGatewayCli(options) {
244
271
  if (!health.ok) throw new Error(`gateway health returned HTTP ${health.status}`);
245
272
  const models = await fetch(gatewayRoutes.models(gatewayUrl), {
246
273
  ...requestOptions,
247
- headers: { authorization: `Bearer ${pat}` },
274
+ headers: { authorization: `Bearer ${bearerCredential(pat)}` },
248
275
  });
249
276
  if (!models.ok) throw new Error(`gateway authentication returned HTTP ${models.status}`);
250
277
  const body = await models.json();
@@ -353,6 +380,7 @@ export function createGatewayCli(options) {
353
380
  const config = loadConfig();
354
381
  if (!config?.pat || !config?.gatewayUrl) throw new Error(`not configured; run \`${brand.cli.command} setup\``);
355
382
  print(`Gateway: ${config.gatewayUrl}`);
383
+ if (brand.tenant.defaultId) print(`Tenant: ${brand.tenant.defaultId}`);
356
384
  print(`PAT: ${maskSecret(config.pat)}`);
357
385
  print(`Profiles: ${profileRoot}`);
358
386
  try {
@@ -367,7 +395,7 @@ export function createGatewayCli(options) {
367
395
  async function cmdToken() {
368
396
  const config = loadConfig();
369
397
  if (!config?.pat) throw new Error(`not authenticated; run \`${brand.cli.command} setup\` or \`${brand.cli.command} auth\``);
370
- output.write(`${config.pat}\n`);
398
+ output.write(`${bearerCredential(config.pat)}\n`);
371
399
  return 0;
372
400
  }
373
401
 
@@ -408,7 +436,7 @@ export function createGatewayCli(options) {
408
436
  const profile = ensureClaudeProfile(config.gatewayUrl);
409
437
  childEnvironment.CLAUDE_CONFIG_DIR = profile.configDir;
410
438
  childEnvironment.ANTHROPIC_BASE_URL = gatewayRoutes.anthropic(config.gatewayUrl);
411
- childEnvironment.ANTHROPIC_AUTH_TOKEN = config.pat;
439
+ childEnvironment.ANTHROPIC_AUTH_TOKEN = bearerCredential(config.pat);
412
440
  } else if (tool === "codex") {
413
441
  const profile = ensureCodexProfile(config.gatewayUrl);
414
442
  childEnvironment.CODEX_HOME = profile.codexHome;