tileflow 0.1.0-alpha.3 → 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.
- package/README.md +10 -1
- package/dist/index.js +54 -20
- package/package.json +5 -5
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
|
-
|
|
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
|
-
|
|
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}.`);
|
|
@@ -7004,17 +7035,15 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
7004
7035
|
phase: "hosted-validation"
|
|
7005
7036
|
});
|
|
7006
7037
|
}
|
|
7007
|
-
const
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
({ severity }) => severity === "warning"
|
|
7012
|
-
)
|
|
7013
|
-
)
|
|
7014
|
-
},
|
|
7015
|
-
process.cwd(),
|
|
7016
|
-
{ code: "VALIDATION_WARNING", phase: "theme-audit" }
|
|
7038
|
+
const themeWarnings = mapNames.flatMap(
|
|
7039
|
+
(mapName) => auditTileflowMapThemeValues(project.maps[mapName]).filter(
|
|
7040
|
+
({ severity }) => severity === "warning"
|
|
7041
|
+
)
|
|
7017
7042
|
);
|
|
7043
|
+
const diagnostics = themeWarnings.length === 0 ? [] : createTileflowStructuredDiagnostics2({ diagnostics: themeWarnings }, process.cwd(), {
|
|
7044
|
+
code: "VALIDATION_WARNING",
|
|
7045
|
+
phase: "theme-audit"
|
|
7046
|
+
});
|
|
7018
7047
|
const checks = [
|
|
7019
7048
|
"Config schema",
|
|
7020
7049
|
"Icon asset closure",
|
|
@@ -8122,7 +8151,7 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
8122
8151
|
process.exitCode = 1;
|
|
8123
8152
|
return null;
|
|
8124
8153
|
}
|
|
8125
|
-
if (profile.value
|
|
8154
|
+
if (!apiProfileCanTargetMap(profile.value, requestedMapId)) {
|
|
8126
8155
|
if (!behavior.silent) logError("This API key belongs to another Map.");
|
|
8127
8156
|
process.exitCode = 1;
|
|
8128
8157
|
return null;
|
|
@@ -8135,6 +8164,11 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
8135
8164
|
process.exitCode = 1;
|
|
8136
8165
|
return null;
|
|
8137
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
|
+
}
|
|
8138
8172
|
if (requested && projectReference(profile.value) !== options.project) {
|
|
8139
8173
|
if (!behavior.silent) {
|
|
8140
8174
|
logError(
|
|
@@ -8154,7 +8188,7 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
8154
8188
|
const allowStoredCredential = behavior.allowStoredCredential !== false && allowsStoredDeployCredential(resolveDeploySource(process.env));
|
|
8155
8189
|
if (!allowStoredCredential) {
|
|
8156
8190
|
if (!behavior.silent) {
|
|
8157
|
-
logError("CI requires an explicit
|
|
8191
|
+
logError("CI requires an explicit Tileflow credential with access to the target Map.");
|
|
8158
8192
|
printNextSteps([
|
|
8159
8193
|
`Set ${pc3.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
|
|
8160
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.
|
|
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",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@hono/node-server": "2.0.10",
|
|
38
|
-
"@tileflow/capture": ">=0.1.0-alpha.
|
|
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.
|
|
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
|
-
"@tileflow/dev": ">=0.1.0-alpha.
|
|
43
|
-
"@tileflow/maps": ">=0.1.0-alpha.
|
|
42
|
+
"@tileflow/dev": ">=0.1.0-alpha.23 <0.1.0-beta.0",
|
|
43
|
+
"@tileflow/maps": ">=0.1.0-alpha.3 <0.1.0-beta.0",
|
|
44
44
|
"commander": "^15.0.0",
|
|
45
45
|
"hono": "4.13.5",
|
|
46
46
|
"picocolors": "^1.1.1",
|