tileflow 0.1.0-alpha.4 → 0.1.0-alpha.5

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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +46 -10
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -592,7 +592,7 @@ An explicit key or `TILEFLOW_API_KEY` never uses the saved account session. The
592
592
  `--map-id` that does not match the key. Read-only hosted icon comparison validates the same binding
593
593
  before loading its baseline.
594
594
 
595
- The CI key grants only `styles:write` and `status:read`. It cannot upload
595
+ A deploy-only key grants `styles:write` and `status:read`. Without additional permissions it cannot upload
596
596
  datasets or render images. Give it an expiration, rotate the repository secret
597
597
  before it expires, and avoid non-expiring CI keys.
598
598
 
@@ -795,3 +795,12 @@ GitHub Action, an atomic multi-map deploy set, or a Tileflow-hosted remote
795
795
  builder.
796
796
 
797
797
  Docs: https://tileflow.dev/docs/deploy
798
+
799
+ ### Team credentials and explicit Map targets
800
+
801
+ A unified Team credential can combine data and Map operations. Use `TILEFLOW_API_KEY` and an
802
+ explicit `--map-id` for deployment, status and hosted icon comparisons. The CLI sends that target
803
+ with style, icon and font requests and verifies the credential covers it. The Hosted API must
804
+ support unified credentials. All Maps includes future Maps; selected Maps is an explicit list.
805
+ A legacy Map key keeps its original binding. Personal CLI login remains separate from machine
806
+ credentials. Credential administration and Team management use the Hosted API or dashboard.
package/dist/index.js CHANGED
@@ -2301,6 +2301,10 @@ async function readBoundedResponseText(response, label) {
2301
2301
  // src/hosted-client.ts
2302
2302
  var defaultHostedRequestTimeoutMs = 3e4;
2303
2303
  var maximumHostedRequestTimeoutMs = 6e4;
2304
+ function apiProfileCanTargetMap(profile, mapId) {
2305
+ if (!/^map_[A-Za-z0-9_-]{16}$/u.test(mapId)) return false;
2306
+ return profile.credentialType === "project_api_key" ? profile.mapId === mapId : profile.mapAccess?.mode === "all" || profile.mapAccess?.mode === "selected" && profile.mapAccess.ids.includes(mapId);
2307
+ }
2304
2308
  async function requestHostedJson(apiUrl, path, init = {}, options = {}) {
2305
2309
  return withHostedResponse(apiUrl, path, init, options, async (response) => {
2306
2310
  const source = await readBoundedResponseText(response, "Hosted API");
@@ -2397,6 +2401,28 @@ async function validateApiKey(apiUrl, apiKey, options = {}) {
2397
2401
  return { error: `API key validation failed (${response.status}).`, ok: false };
2398
2402
  }
2399
2403
  const body = asRecord(response.body);
2404
+ if (response.json && body.credentialType === "team_api_key") {
2405
+ const mapAccess = asRecord(body.mapAccess);
2406
+ const validMaps = body.mapAccess === null || mapAccess.mode === "all" && Object.keys(mapAccess).length === 1 || mapAccess.mode === "selected" && Object.keys(mapAccess).length === 2 && Array.isArray(mapAccess.ids) && mapAccess.ids.length > 0 && mapAccess.ids.length <= 1e3 && mapAccess.ids.every(
2407
+ (id) => typeof id === "string" && /^map_[A-Za-z0-9_-]{16}$/u.test(id)
2408
+ ) && new Set(mapAccess.ids).size === mapAccess.ids.length;
2409
+ if (typeof body.apiKeyId !== "string" || !isProjectIdentity(body.organization) || body.project !== null || body.projectId !== null || body.mapId !== null || !validMaps || !Array.isArray(body.scopes) || !body.scopes.every((scope) => typeof scope === "string")) {
2410
+ return { error: "API key validation returned an invalid response.", ok: false };
2411
+ }
2412
+ return {
2413
+ ok: true,
2414
+ value: {
2415
+ apiKeyId: body.apiKeyId,
2416
+ credentialType: "team_api_key",
2417
+ organization: body.organization,
2418
+ mapId: null,
2419
+ project: null,
2420
+ projectId: null,
2421
+ scopes: body.scopes,
2422
+ mapAccess: body.mapAccess === null ? null : mapAccess.mode === "all" ? { mode: "all" } : { mode: "selected", ids: mapAccess.ids }
2423
+ }
2424
+ };
2425
+ }
2400
2426
  if (!response.json || typeof body.apiKeyId !== "string" || body.credentialType !== "project_api_key" || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || !isProjectIdentity(body.organization) || !isProjectIdentity(body.project) || typeof body.projectId !== "string" || body.projectId !== body.project.id || !Array.isArray(body.scopes)) {
2401
2427
  return { error: "API key validation returned an invalid response.", ok: false };
2402
2428
  }
@@ -2526,7 +2552,7 @@ async function publishHostedStyle(api, body, responseLabel, options = {}) {
2526
2552
  "/v1/styles",
2527
2553
  {
2528
2554
  ...jsonRequest("POST", body),
2529
- headers: { ...authorizationHeaders(api.apiKey), "Content-Type": "application/json" }
2555
+ headers: { ...authorizationHeaders(api.apiKey, api.mapId), "Content-Type": "application/json" }
2530
2556
  },
2531
2557
  options
2532
2558
  );
@@ -2554,7 +2580,7 @@ async function uploadHostedIconPackage(api, iconPackage, options = {}) {
2554
2580
  const response = await requestHostedJson(
2555
2581
  api.apiUrl,
2556
2582
  `/v1/icon-packages/${encodeURIComponent(iconPackage.contentHash)}`,
2557
- { body: formData, headers: authorizationHeaders(api.apiKey), method: "PUT" },
2583
+ { body: formData, headers: authorizationHeaders(api.apiKey, api.mapId), method: "PUT" },
2558
2584
  options
2559
2585
  );
2560
2586
  if (!response.ok) return { ok: false, status: response.status };
@@ -2591,7 +2617,7 @@ async function uploadHostedFontBundle(api, bundle, options = {}) {
2591
2617
  const response = await requestHostedJson(
2592
2618
  api.apiUrl,
2593
2619
  `/v1/font-bundles/${encodeURIComponent(bundle.contentHash)}`,
2594
- { body: formData, headers: authorizationHeaders(api.apiKey), method: "PUT" },
2620
+ { body: formData, headers: authorizationHeaders(api.apiKey, api.mapId), method: "PUT" },
2595
2621
  options
2596
2622
  );
2597
2623
  if (!response.ok) return { ok: false, status: response.status };
@@ -2610,7 +2636,7 @@ async function fetchHostedMapStatus(api, options = {}) {
2610
2636
  const response = await requestHostedJson(
2611
2637
  api.apiUrl,
2612
2638
  "/v1/status",
2613
- { headers: authorizationHeaders(api.apiKey) },
2639
+ { headers: authorizationHeaders(api.apiKey, api.mapId) },
2614
2640
  options
2615
2641
  );
2616
2642
  if (!response.ok) throw new Error(`Status failed: ${response.status}.`);
@@ -2707,11 +2733,16 @@ function jsonRequest(method, body) {
2707
2733
  method
2708
2734
  };
2709
2735
  }
2710
- function authorizationHeaders(token) {
2736
+ function authorizationHeaders(token, mapId) {
2711
2737
  if (!token || token.length > 8192 || /[\p{Cc}]/u.test(token)) {
2712
2738
  throw new TypeError("Hosted authorization credential is invalid.");
2713
2739
  }
2714
- return { Authorization: `Bearer ${token}` };
2740
+ if (mapId !== void 0 && !/^map_[A-Za-z0-9_-]{16}$/u.test(mapId))
2741
+ throw new TypeError("Hosted Map ID is invalid.");
2742
+ return {
2743
+ Authorization: `Bearer ${token}`,
2744
+ ...mapId === void 0 ? {} : { "X-Tileflow-Map-Id": mapId }
2745
+ };
2715
2746
  }
2716
2747
  function parseResponse(response, schema, label) {
2717
2748
  if (!response.json) throw new Error(`${label} returned invalid JSON.`);
@@ -3453,7 +3484,7 @@ async function runIconDiff(options, dependencies) {
3453
3484
  });
3454
3485
  const binding = compiled.bindings.find((candidate) => candidate.mapName === environment);
3455
3486
  const proposedPackage = binding ? compiled.packages.find((candidate) => candidate.contentHash === binding.packageHash) ?? null : null;
3456
- const baseline = await readBaseline(api, environment);
3487
+ const baseline = await readBaseline({ ...api, mapId: options.mapId }, environment);
3457
3488
  const beforeManifest = baseline.baseline?.package?.manifest ?? null;
3458
3489
  const iconDiff = diffTileflowIconPackageManifests(
3459
3490
  beforeManifest,
@@ -3511,7 +3542,7 @@ async function readBaseline(api, environment) {
3511
3542
  const response = await requestHostedJson(
3512
3543
  api.apiUrl,
3513
3544
  `/v1/icon-packages/baseline/${encodeURIComponent(environment)}`,
3514
- { headers: { Authorization: `Bearer ${api.apiKey}` } }
3545
+ { headers: { Authorization: `Bearer ${api.apiKey}`, "X-Tileflow-Map-Id": api.mapId } }
3515
3546
  );
3516
3547
  if (!response.ok) {
3517
3548
  throw new Error(`Icon diff baseline failed: ${response.status}.`);
@@ -8120,7 +8151,7 @@ async function requireApiOptions(options, behavior = {}) {
8120
8151
  process.exitCode = 1;
8121
8152
  return null;
8122
8153
  }
8123
- if (profile.value.mapId !== requestedMapId) {
8154
+ if (!apiProfileCanTargetMap(profile.value, requestedMapId)) {
8124
8155
  if (!behavior.silent) logError("This API key belongs to another Map.");
8125
8156
  process.exitCode = 1;
8126
8157
  return null;
@@ -8133,6 +8164,11 @@ async function requireApiOptions(options, behavior = {}) {
8133
8164
  process.exitCode = 1;
8134
8165
  return null;
8135
8166
  }
8167
+ if (profile.value.credentialType === "team_api_key") {
8168
+ if (!behavior.silent) logError("Team credentials require an explicit --map-id.");
8169
+ process.exitCode = 1;
8170
+ return null;
8171
+ }
8136
8172
  if (requested && projectReference(profile.value) !== options.project) {
8137
8173
  if (!behavior.silent) {
8138
8174
  logError(
@@ -8152,7 +8188,7 @@ async function requireApiOptions(options, behavior = {}) {
8152
8188
  const allowStoredCredential = behavior.allowStoredCredential !== false && allowsStoredDeployCredential(resolveDeploySource(process.env));
8153
8189
  if (!allowStoredCredential) {
8154
8190
  if (!behavior.silent) {
8155
- logError("CI requires an explicit Map-scoped Tileflow API key.");
8191
+ logError("CI requires an explicit Tileflow credential with access to the target Map.");
8156
8192
  printNextSteps([
8157
8193
  `Set ${pc3.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
8158
8194
  "The saved personal account session is never used in CI."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileflow",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "CLI-only tools for Tileflow config, capture, visual testing, and deploy.",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "@hono/node-server": "2.0.10",
38
38
  "@tileflow/capture": ">=0.1.0-alpha.22 <0.1.0-beta.0",
39
39
  "@tileflow/coordinates": ">=0.1.0-alpha.0 <0.1.0-beta.0",
40
- "@tileflow/coordinates-runtime": ">=0.1.0-alpha.1 <0.1.0-beta.0",
40
+ "@tileflow/coordinates-runtime": ">=0.1.0-alpha.2 <0.1.0-beta.0",
41
41
  "@tileflow/core": ">=0.1.0-alpha.24 <0.1.0-beta.0",
42
42
  "@tileflow/dev": ">=0.1.0-alpha.23 <0.1.0-beta.0",
43
43
  "@tileflow/maps": ">=0.1.0-alpha.3 <0.1.0-beta.0",