tileflow 0.1.0-alpha.20 → 0.1.0-alpha.22

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.
@@ -0,0 +1,2145 @@
1
+ // src/account-session.ts
2
+ import { randomBytes } from "crypto";
3
+ import { chmod, mkdir, open, readFile, rename, unlink } from "fs/promises";
4
+ import { homedir } from "os";
5
+ import { dirname, resolve } from "path";
6
+ import { z } from "zod";
7
+ var rejectedAccountSessionMessage = "The saved Tileflow account session is no longer valid. Run tileflow login to authorize this machine again.";
8
+ var maximumAuthFileBytes = 1024 * 1024;
9
+ var maximumOrigins = 16;
10
+ var accountSchema = z.object({
11
+ email: z.email().max(320),
12
+ id: z.string().min(1).max(200),
13
+ name: z.string().min(1).max(200)
14
+ }).strict();
15
+ var apiOriginSchema = z.url().max(2048).refine((value) => normalizeApiOrigin(value) === value);
16
+ var sessionSchema = z.object({
17
+ account: accountSchema,
18
+ accountSession: z.string().regex(/^tf_session_[0-9a-f]{64}$/u),
19
+ apiOrigin: apiOriginSchema,
20
+ createdAt: z.iso.datetime({ offset: true }),
21
+ expiresAt: z.iso.datetime({ offset: true }),
22
+ sessionId: z.string().min(1).max(200)
23
+ }).strict().refine((value) => Date.parse(value.expiresAt) > Date.parse(value.createdAt), {
24
+ message: "session expiry must follow creation"
25
+ });
26
+ var authConfigSchema = z.object({
27
+ sessions: z.record(z.string().max(2048), sessionSchema),
28
+ version: z.literal(2)
29
+ }).strict().superRefine((value, context) => {
30
+ if (Object.keys(value.sessions).length > maximumOrigins) {
31
+ context.addIssue({ code: "custom", message: "too many API origins", path: ["sessions"] });
32
+ }
33
+ for (const [origin, session] of Object.entries(value.sessions)) {
34
+ if (normalizeApiOrigin(origin) !== origin || session.apiOrigin !== origin) {
35
+ context.addIssue({ code: "custom", message: "origin mismatch", path: ["sessions", origin] });
36
+ }
37
+ }
38
+ });
39
+ var legacyAuthConfigSchema = z.object({
40
+ apiKey: z.string().min(1).max(4096),
41
+ apiUrl: z.url().max(2048).refine((value) => {
42
+ try {
43
+ normalizeApiOrigin(value);
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ })
49
+ }).strict();
50
+ function emptyAuthConfig() {
51
+ return Object.freeze({ sessions: Object.freeze({}), version: 2 });
52
+ }
53
+ async function readAuthFile(path = authConfigPath()) {
54
+ let source;
55
+ try {
56
+ source = await readFile(path, "utf8");
57
+ } catch (error) {
58
+ return isMissingFile(error) ? { kind: "empty" } : { kind: "invalid" };
59
+ }
60
+ if (Buffer.byteLength(source, "utf8") > maximumAuthFileBytes) return { kind: "invalid" };
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(source);
64
+ } catch {
65
+ return { kind: "invalid" };
66
+ }
67
+ const config = authConfigSchema.safeParse(parsed);
68
+ if (config.success) return { config: freezeConfig(config.data), kind: "sessions" };
69
+ return legacyAuthConfigSchema.safeParse(parsed).success ? { kind: "legacy" } : { kind: "invalid" };
70
+ }
71
+ async function prepareAccountLogin(path = authConfigPath()) {
72
+ const state = await readAuthFile(path);
73
+ if (state.kind === "invalid") {
74
+ throw new Error("Tileflow auth state is invalid; no changes were made.");
75
+ }
76
+ const directory = dirname(path);
77
+ const probePath = `${path}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`;
78
+ await mkdir(directory, { mode: 448, recursive: true });
79
+ const handle = await open(probePath, "wx", 384);
80
+ try {
81
+ await handle.sync();
82
+ } finally {
83
+ await handle.close();
84
+ await unlink(probePath);
85
+ }
86
+ return state;
87
+ }
88
+ async function loadAuthConfig(path = authConfigPath()) {
89
+ const state = await readAuthFile(path);
90
+ if (state.kind === "empty") return emptyAuthConfig();
91
+ if (state.kind === "sessions") return state.config;
92
+ if (state.kind === "legacy") {
93
+ throw new Error(
94
+ "Tileflow CLI login uses an older format. Run tileflow login to sign in again."
95
+ );
96
+ }
97
+ throw new Error("Tileflow auth state is invalid; no changes were made.");
98
+ }
99
+ async function installAccountSession(session, path = authConfigPath()) {
100
+ const normalized = sessionSchema.parse({
101
+ ...session,
102
+ apiOrigin: normalizeApiOrigin(session.apiOrigin)
103
+ });
104
+ const state = await readAuthFile(path);
105
+ if (state.kind === "invalid") {
106
+ throw new Error("Tileflow auth state is invalid; no changes were made.");
107
+ }
108
+ const current = state.kind === "sessions" ? state.config : emptyAuthConfig();
109
+ const sessions = { ...current.sessions, [normalized.apiOrigin]: normalized };
110
+ if (Object.keys(sessions).length > maximumOrigins) {
111
+ throw new Error("Tileflow CLI API-origin limit reached.");
112
+ }
113
+ const config = freezeConfig({ sessions, version: 2 });
114
+ await writeAuthFileAtomic(config, path);
115
+ return config;
116
+ }
117
+ function resolveAccountSession(config, apiUrl, now = /* @__PURE__ */ new Date()) {
118
+ const origin = normalizeApiOrigin(apiUrl);
119
+ const session = config.sessions[origin];
120
+ if (!session) return { kind: "missing" };
121
+ return Date.parse(session.expiresAt) <= now.getTime() ? { kind: "expired" } : { kind: "selected", origin, session };
122
+ }
123
+ function removeAccountSession(config, apiUrl) {
124
+ const origin = normalizeApiOrigin(apiUrl);
125
+ const sessions = { ...config.sessions };
126
+ const removed = sessions[origin];
127
+ delete sessions[origin];
128
+ return { config: freezeConfig({ sessions, version: 2 }), removed };
129
+ }
130
+ async function writeAuthFileAtomic(config, path = authConfigPath()) {
131
+ const normalized = normalizeConfig(authConfigSchema.parse(config));
132
+ const source = `${JSON.stringify(normalized, null, 2)}
133
+ `;
134
+ if (Buffer.byteLength(source, "utf8") > maximumAuthFileBytes) {
135
+ throw new Error("Tileflow auth state exceeds its safe size limit.");
136
+ }
137
+ const directory = dirname(path);
138
+ const temporaryPath = `${path}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`;
139
+ await mkdir(directory, { mode: 448, recursive: true });
140
+ await chmod(directory, 448);
141
+ let handle;
142
+ try {
143
+ handle = await open(temporaryPath, "wx", 384);
144
+ await handle.writeFile(source, "utf8");
145
+ await handle.sync();
146
+ await handle.close();
147
+ handle = void 0;
148
+ await chmod(temporaryPath, 384);
149
+ await rename(temporaryPath, path);
150
+ await chmod(path, 384);
151
+ await syncDirectory(directory);
152
+ } catch (error) {
153
+ await handle?.close().catch(() => void 0);
154
+ await unlink(temporaryPath).catch(() => void 0);
155
+ throw error;
156
+ }
157
+ }
158
+ async function removeAuthFile(path = authConfigPath()) {
159
+ await unlink(path).catch((error) => {
160
+ if (!isMissingFile(error)) throw error;
161
+ });
162
+ }
163
+ function normalizeApiOrigin(value) {
164
+ const url = new URL(value);
165
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") {
166
+ throw new Error("Tileflow API URL must be a safe HTTP origin.");
167
+ }
168
+ return url.origin;
169
+ }
170
+ function parseProjectReference(value) {
171
+ if (typeof value !== "string") return null;
172
+ const match = /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})\/([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u.exec(
173
+ value
174
+ );
175
+ return match ? { organizationSlug: match[1], projectSlug: match[2] } : null;
176
+ }
177
+ function authConfigPath() {
178
+ return resolve(homedir(), ".tileflow", "config.json");
179
+ }
180
+ function normalizeConfig(config) {
181
+ return {
182
+ sessions: Object.fromEntries(
183
+ Object.entries(config.sessions).sort(([left], [right]) => compareCodeUnits(left, right)).map(([origin, session]) => [origin, { ...session, apiOrigin: origin }])
184
+ ),
185
+ version: 2
186
+ };
187
+ }
188
+ function freezeConfig(config) {
189
+ const normalized = normalizeConfig(config);
190
+ const sessions = Object.fromEntries(
191
+ Object.entries(normalized.sessions).map(([origin, session]) => [
192
+ origin,
193
+ Object.freeze({ ...session, account: Object.freeze({ ...session.account }) })
194
+ ])
195
+ );
196
+ return Object.freeze({ sessions: Object.freeze(sessions), version: 2 });
197
+ }
198
+ async function syncDirectory(path) {
199
+ if (process.platform === "win32") return;
200
+ const directory = await open(path, "r");
201
+ try {
202
+ await directory.sync();
203
+ } finally {
204
+ await directory.close();
205
+ }
206
+ }
207
+ function isMissingFile(error) {
208
+ return Boolean(
209
+ error && typeof error === "object" && error.code === "ENOENT"
210
+ );
211
+ }
212
+ function compareCodeUnits(left, right) {
213
+ return left < right ? -1 : left > right ? 1 : 0;
214
+ }
215
+
216
+ // src/cli-program.ts
217
+ import { Command } from "commander";
218
+ import pc from "picocolors";
219
+ function createCliProgram() {
220
+ return new Command().name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(pc.gray("\nRun tileflow <command> --help for usage.")).showSuggestionAfterError().addHelpText("after", "\n -V, --version output the version number\n");
221
+ }
222
+
223
+ // src/hosted-options.ts
224
+ import { Option } from "commander";
225
+ function hostedApiKeyOption(description = "Tileflow API key") {
226
+ return new Option("--api-key <key>", description).env("TILEFLOW_API_KEY");
227
+ }
228
+ function hostedApiUrlOption() {
229
+ return new Option("--api-url <url>", "Tileflow API URL").env("TILEFLOW_API_URL");
230
+ }
231
+ function isHostedApiKey(value) {
232
+ return /^tf_live_[0-9a-f]{48}$/u.test(value);
233
+ }
234
+ function allowsStoredAccountSession(env = process.env) {
235
+ return !env.CI && env.GITHUB_ACTIONS !== "true" && env.GITLAB_CI !== "true";
236
+ }
237
+
238
+ // src/hosted-client.ts
239
+ import { serializeTileflowFontBundleManifest } from "@tileflow/core";
240
+ import { compactTileflowDeploymentArtifact, tileflowDeploymentWebBytes } from "@tileflow/core/build";
241
+
242
+ // src/hosted-response.ts
243
+ import { z as z2 } from "zod";
244
+ import { tileflowIconPackageLimits } from "@tileflow/core";
245
+ var maximumHostedResponseBytes = 1024 * 1024;
246
+ var safeTextSchema = z2.string().min(1).max(300).refine((value) => !/[\p{Cc}]/u.test(value));
247
+ var safeIdentifierSchema = z2.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u);
248
+ var safeStorageKeySchema = z2.string().min(1).max(512).regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/u).refine((value) => value.split("/").every((segment) => segment !== "." && segment !== ".."));
249
+ var publicHttpUrlSchema = z2.url().max(2048).refine((value) => {
250
+ const url = new URL(value);
251
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && !url.hash;
252
+ });
253
+ var hostedStyleDeploymentResponseSchema = z2.object({
254
+ changed: z2.boolean().optional(),
255
+ deploymentId: safeIdentifierSchema.optional(),
256
+ mapId: z2.string().regex(/^map_[A-Za-z0-9_-]{16}$/u),
257
+ familyUrl: publicHttpUrlSchema.optional(),
258
+ themes: z2.record(
259
+ safeIdentifierSchema,
260
+ z2.object({
261
+ styleId: safeIdentifierSchema.optional(),
262
+ styleUrl: publicHttpUrlSchema
263
+ }).strict()
264
+ ).refine((themes) => Object.keys(themes).length > 0),
265
+ version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
266
+ /**
267
+ * The Team 3D models the map places, each at its published current version, and whether that
268
+ * version was prepared from the same glTF binaries as the map's own files.
269
+ */
270
+ teamModels: z2.array(
271
+ z2.object({
272
+ model: z2.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u).max(64),
273
+ version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
274
+ sources: z2.enum(["match", "differ", "unknown"])
275
+ }).strict()
276
+ ).max(256).optional()
277
+ }).strict();
278
+ var hostedIconPackageResponseSchema = z2.object({
279
+ changed: z2.boolean().optional(),
280
+ contentHash: z2.string().regex(/^[a-f0-9]{64}$/u),
281
+ iconCount: z2.number().int().positive().max(tileflowIconPackageLimits.maxIconCount),
282
+ id: z2.string().regex(/^icp_[A-Za-z0-9_-]{16,64}$/u),
283
+ spriteUrl: publicHttpUrlSchema,
284
+ totalBytes: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER)
285
+ });
286
+ var hostedFontBundleResponseSchema = z2.object({
287
+ baseUrl: publicHttpUrlSchema,
288
+ changed: z2.boolean().optional(),
289
+ contentHash: z2.string().regex(/^[a-f0-9]{64}$/u),
290
+ fontFaceCount: z2.number().int().positive().max(16),
291
+ id: z2.string().regex(/^fnb_[A-Za-z0-9_-]{16,64}$/u),
292
+ totalBytes: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER)
293
+ });
294
+ var hostedStatusStyleSchema = z2.object({
295
+ environment: safeTextSchema,
296
+ key: safeStorageKeySchema,
297
+ mapId: safeIdentifierSchema,
298
+ size: z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
299
+ uploaded: z2.iso.datetime({ offset: true })
300
+ });
301
+ var hostedMapStatusSchema = z2.object({
302
+ mapId: z2.string().regex(/^map_[A-Za-z0-9_-]{16}$/u),
303
+ styles: z2.array(hostedStatusStyleSchema).max(1e4)
304
+ });
305
+ async function readBoundedResponseText(response, label) {
306
+ const contentLength = response.headers.get("content-length");
307
+ if (contentLength && /^\d+$/u.test(contentLength) && Number(contentLength) > maximumHostedResponseBytes) {
308
+ throw new Error(`${label} response exceeded the safe size limit.`);
309
+ }
310
+ if (!response.body) return "";
311
+ const reader = response.body.getReader();
312
+ const chunks = [];
313
+ let byteLength = 0;
314
+ try {
315
+ while (true) {
316
+ const { done, value } = await reader.read();
317
+ if (done) break;
318
+ if (!value) continue;
319
+ byteLength += value.byteLength;
320
+ if (byteLength > maximumHostedResponseBytes) {
321
+ await reader.cancel().catch(() => void 0);
322
+ throw new Error(`${label} response exceeded the safe size limit.`);
323
+ }
324
+ chunks.push(value);
325
+ }
326
+ } finally {
327
+ reader.releaseLock();
328
+ }
329
+ const bytes = new Uint8Array(byteLength);
330
+ let offset = 0;
331
+ for (const chunk of chunks) {
332
+ bytes.set(chunk, offset);
333
+ offset += chunk.byteLength;
334
+ }
335
+ try {
336
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
337
+ } catch {
338
+ throw new Error(`${label} returned invalid UTF-8.`);
339
+ }
340
+ }
341
+
342
+ // src/hosted-client.ts
343
+ var defaultHostedRequestTimeoutMs = 3e4;
344
+ var maximumHostedRequestTimeoutMs = 6e4;
345
+ function apiProfileCanTargetMap(profile, mapId) {
346
+ if (!/^map_[A-Za-z0-9_-]{16}$/u.test(mapId)) return false;
347
+ return profile.credentialType === "project_api_key" ? profile.mapId === mapId : profile.mapAccess?.mode === "all" || profile.mapAccess?.mode === "selected" && profile.mapAccess.ids.includes(mapId);
348
+ }
349
+ async function requestHostedJson(apiUrl, path, init = {}, options = {}) {
350
+ return withHostedResponse(apiUrl, path, init, options, async (response) => {
351
+ const retryAfter = response.headers.get("retry-after");
352
+ const retryAfterMs = retryAfter === null ? void 0 : /^\d+$/u.test(retryAfter) ? Number(retryAfter) * 1e3 : Math.max(0, Date.parse(retryAfter) - Date.now());
353
+ const metadata = {
354
+ ok: response.ok,
355
+ status: response.status,
356
+ ...Number.isFinite(retryAfterMs) ? { retryAfterMs } : {}
357
+ };
358
+ const source = await readBoundedResponseText(response, "Hosted API");
359
+ if (!source) return { body: null, json: true, ...metadata };
360
+ try {
361
+ return {
362
+ body: JSON.parse(source),
363
+ json: true,
364
+ ...metadata
365
+ };
366
+ } catch {
367
+ return { body: null, json: false, ...metadata };
368
+ }
369
+ });
370
+ }
371
+ async function startDeviceAuthorization(appUrl, body, options = {}) {
372
+ const response = await requestHostedJson(
373
+ appUrl,
374
+ "/api/cli/device/start",
375
+ jsonRequest("POST", body),
376
+ options
377
+ );
378
+ if (!response.ok) {
379
+ throw new Error(`Could not start CLI authorization (${response.status}).`);
380
+ }
381
+ if (!isDeviceAuthorization(response.body)) {
382
+ throw new Error("Could not start CLI authorization (invalid response).");
383
+ }
384
+ const appOrigin = normalizeApiOrigin(appUrl);
385
+ if (new URL(response.body.verificationUri).origin !== appOrigin || new URL(response.body.verificationUriComplete).origin !== appOrigin) {
386
+ throw new Error("Could not start CLI authorization (unexpected verification origin).");
387
+ }
388
+ return response.body;
389
+ }
390
+ async function pollDeviceToken(appUrl, authorization, input, options = {}) {
391
+ const now = input.now ?? Date.now;
392
+ const expiresAt = input.expiresAt ?? now() + authorization.expiresIn * 1e3;
393
+ let intervalMs = Math.max(authorization.interval, 1) * 1e3;
394
+ let delayMs = intervalMs;
395
+ const wait = input.sleep ?? ((milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)));
396
+ while (now() < expiresAt) {
397
+ options.signal?.throwIfAborted();
398
+ await wait(Math.min(delayMs, expiresAt - now()));
399
+ options.signal?.throwIfAborted();
400
+ if (now() >= expiresAt) break;
401
+ let response;
402
+ try {
403
+ response = await requestHostedJson(
404
+ appUrl,
405
+ "/api/cli/device/token",
406
+ jsonRequest("POST", {
407
+ codeVerifier: input.codeVerifier,
408
+ deviceCode: authorization.deviceCode
409
+ }),
410
+ {
411
+ ...options,
412
+ timeoutMs: Math.min(
413
+ options.timeoutMs ?? defaultHostedRequestTimeoutMs,
414
+ expiresAt - now()
415
+ )
416
+ }
417
+ );
418
+ } catch (error2) {
419
+ options.signal?.throwIfAborted();
420
+ if (!(error2 instanceof TypeError) && !(error2 instanceof Error && error2.name === "TimeoutError"))
421
+ throw error2;
422
+ delayMs = Math.min(Math.max(delayMs * 2, intervalMs), 6e4);
423
+ continue;
424
+ }
425
+ if (response.ok && isDeviceToken(response.body)) return response.body;
426
+ const error = asRecord(response.body).error;
427
+ if (error === "access_denied") throw new Error("CLI authorization was denied.");
428
+ if (error === "expired_token") break;
429
+ if (error === "slow_down" || response.status === 429) {
430
+ intervalMs += 5e3;
431
+ delayMs = Math.max(intervalMs, response.retryAfterMs ?? 0);
432
+ continue;
433
+ }
434
+ if (error === "authorization_pending") {
435
+ delayMs = intervalMs;
436
+ continue;
437
+ }
438
+ if ([408, 500, 502, 503, 504].includes(response.status)) {
439
+ delayMs = Math.max(Math.min(delayMs * 2, 6e4), response.retryAfterMs ?? 0);
440
+ continue;
441
+ }
442
+ throw new Error(`CLI authorization failed (${response.status}).`);
443
+ }
444
+ throw new Error("CLI authorization expired. Run `tileflow login` again.");
445
+ }
446
+ async function validateAccountSession(session, options = {}) {
447
+ const response = await requestHostedJson(
448
+ session.apiOrigin,
449
+ "/v1/cli/account",
450
+ { headers: authorizationHeaders(session.accountSession) },
451
+ options
452
+ );
453
+ if (!response.ok) {
454
+ return {
455
+ error: response.status === 401 ? rejectedAccountSessionMessage : `Account session validation failed (${response.status}).`,
456
+ ok: false
457
+ };
458
+ }
459
+ const body = asRecord(response.body);
460
+ const account = asRecord(body.account);
461
+ const sessionProfile = asRecord(body.session);
462
+ if (!response.json || body.schemaVersion !== 1 || !isAccountIdentity(account) || typeof sessionProfile.id !== "string" || sessionProfile.id !== session.sessionId || !validIsoDate(sessionProfile.expiresAt) || sessionProfile.expiresAt !== session.expiresAt || Object.hasOwn(body, "project")) {
463
+ return { error: "Account session validation returned an invalid response.", ok: false };
464
+ }
465
+ return {
466
+ ok: true,
467
+ value: {
468
+ account,
469
+ session: { expiresAt: sessionProfile.expiresAt, id: sessionProfile.id }
470
+ }
471
+ };
472
+ }
473
+ async function validateApiKey(apiUrl, apiKey, options = {}) {
474
+ const response = await requestHostedJson(
475
+ apiUrl,
476
+ "/v1/me",
477
+ { headers: authorizationHeaders(apiKey) },
478
+ options
479
+ );
480
+ if (!response.ok) {
481
+ return { error: `API key validation failed (${response.status}).`, ok: false };
482
+ }
483
+ const body = asRecord(response.body);
484
+ if (response.json && body.credentialType === "team_api_key") {
485
+ const mapAccess = asRecord(body.mapAccess);
486
+ 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(
487
+ (id) => typeof id === "string" && /^map_[A-Za-z0-9_-]{16}$/u.test(id)
488
+ ) && new Set(mapAccess.ids).size === mapAccess.ids.length;
489
+ 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")) {
490
+ return { error: "API key validation returned an invalid response.", ok: false };
491
+ }
492
+ return {
493
+ ok: true,
494
+ value: {
495
+ apiKeyId: body.apiKeyId,
496
+ credentialType: "team_api_key",
497
+ organization: body.organization,
498
+ mapId: null,
499
+ project: null,
500
+ projectId: null,
501
+ scopes: body.scopes,
502
+ mapAccess: body.mapAccess === null ? null : mapAccess.mode === "all" ? { mode: "all" } : { mode: "selected", ids: mapAccess.ids }
503
+ }
504
+ };
505
+ }
506
+ 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)) {
507
+ return { error: "API key validation returned an invalid response.", ok: false };
508
+ }
509
+ return {
510
+ ok: true,
511
+ value: {
512
+ apiKeyId: body.apiKeyId,
513
+ credentialType: body.credentialType,
514
+ mapId: body.mapId,
515
+ organization: body.organization,
516
+ project: body.project,
517
+ projectId: body.projectId,
518
+ scopes: body.scopes.filter((scope) => typeof scope === "string")
519
+ }
520
+ };
521
+ }
522
+ async function requestMapCapability(session, target, scopes, options = {}) {
523
+ let response;
524
+ try {
525
+ response = await requestHostedJson(
526
+ session.apiOrigin,
527
+ "/v1/cli/map-capabilities",
528
+ {
529
+ ...jsonRequest("POST", { ...target, scopes }),
530
+ headers: {
531
+ ...authorizationHeaders(session.accountSession),
532
+ "Content-Type": "application/json"
533
+ }
534
+ },
535
+ options
536
+ );
537
+ } catch (error) {
538
+ return { error: safeTransportError(error, "Map capability request failed."), ok: false };
539
+ }
540
+ if (!response.ok) {
541
+ return {
542
+ error: response.status === 401 ? rejectedAccountSessionMessage : `Map capability request failed (${response.status}).`,
543
+ ok: false
544
+ };
545
+ }
546
+ const body = asRecord(response.body);
547
+ if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || body.mapId !== target.mapId || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
548
+ return { error: "Map capability response was invalid.", ok: false };
549
+ }
550
+ return { capability: body.capability, mapId: body.mapId, ok: true };
551
+ }
552
+ async function requestTeamCapability(session, team, scopes, options = {}) {
553
+ let response;
554
+ try {
555
+ response = await requestHostedJson(
556
+ session.apiOrigin,
557
+ "/v1/cli/team-capabilities",
558
+ {
559
+ ...jsonRequest("POST", { scopes, team }),
560
+ headers: {
561
+ ...authorizationHeaders(session.accountSession),
562
+ "Content-Type": "application/json"
563
+ }
564
+ },
565
+ options
566
+ );
567
+ } catch (error) {
568
+ return { error: safeTransportError(error, "Team capability request failed."), ok: false };
569
+ }
570
+ if (!response.ok) {
571
+ return { error: `Team capability request failed (${response.status}).`, ok: false };
572
+ }
573
+ const body = asRecord(response.body);
574
+ if (!response.json || body.schemaVersion !== 1 || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || body.reference !== team || !isProjectIdentity(body.team) || `@${body.team.slug}` !== team || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
575
+ return { error: "Team capability response was invalid.", ok: false };
576
+ }
577
+ return { capability: body.capability, expiresAt: body.expiresAt, ok: true, team: body.team };
578
+ }
579
+ async function listAccountTeams(session, options = {}) {
580
+ const response = await requestHostedJson(
581
+ session.apiOrigin,
582
+ "/v1/cli/teams",
583
+ { headers: authorizationHeaders(session.accountSession) },
584
+ options
585
+ );
586
+ const body = asRecord(response.body);
587
+ if (!response.ok) return { error: `Team discovery failed (${response.status}).`, ok: false };
588
+ if (!response.json || body.schemaVersion !== 1 || !Array.isArray(body.teams) || !body.teams.every(isProjectIdentity)) {
589
+ return { error: "Team discovery returned an invalid response.", ok: false };
590
+ }
591
+ const teams = [...body.teams].sort(
592
+ (left, right) => left.slug < right.slug ? -1 : left.slug > right.slug ? 1 : 0
593
+ );
594
+ if (new Set(teams.map(({ id }) => id)).size !== teams.length || teams.some((team, index) => index > 0 && teams[index - 1].slug >= team.slug)) {
595
+ return { error: "Team discovery returned unstable ordering.", ok: false };
596
+ }
597
+ return { ok: true, teams };
598
+ }
599
+ async function listMapSetupTeams(session, options = {}) {
600
+ let response;
601
+ try {
602
+ response = await requestHostedJson(
603
+ session.apiOrigin,
604
+ "/v1/cli/map-setup/teams",
605
+ { headers: authorizationHeaders(session.accountSession) },
606
+ options
607
+ );
608
+ } catch (error) {
609
+ return { error: safeTransportError(error, "Team discovery failed."), ok: false };
610
+ }
611
+ if (!response.ok) {
612
+ return {
613
+ error: response.status === 401 ? rejectedAccountSessionMessage : `Team discovery failed (${response.status}).`,
614
+ ok: false
615
+ };
616
+ }
617
+ const body = asRecord(response.body);
618
+ if (!response.json || body.schemaVersion !== 1 || !Array.isArray(body.teams) || !body.teams.every(isMapSetupTeam)) {
619
+ return { error: "Team discovery returned an invalid response.", ok: false };
620
+ }
621
+ return { ok: true, teams: body.teams };
622
+ }
623
+ async function createManagedMapSetup(session, input, options = {}) {
624
+ let response;
625
+ try {
626
+ response = await requestHostedJson(
627
+ session.apiOrigin,
628
+ "/v1/cli/maps",
629
+ {
630
+ ...jsonRequest("POST", input),
631
+ headers: {
632
+ ...authorizationHeaders(session.accountSession),
633
+ "Content-Type": "application/json"
634
+ }
635
+ },
636
+ options
637
+ );
638
+ } catch (error) {
639
+ return {
640
+ code: "setup_failed",
641
+ error: safeTransportError(error, "Map setup request failed."),
642
+ ok: false
643
+ };
644
+ }
645
+ const body = asRecord(response.body);
646
+ if (!response.ok) {
647
+ if (response.status === 401) {
648
+ return { code: "account_session_rejected", error: rejectedAccountSessionMessage, ok: false };
649
+ }
650
+ const code = mapSetupFailureMessages[body.code] ? body.code : null;
651
+ return code ? { code, error: mapSetupFailureMessages[code], ok: false } : { code: "setup_failed", error: `Map setup failed (${response.status}).`, ok: false };
652
+ }
653
+ const map = asRecord(body.map);
654
+ const team = asRecord(body.team);
655
+ const delivery = asRecord(body.delivery);
656
+ if (!response.json || body.schemaVersion !== 1 || typeof map.id !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(map.id) || map.name !== null && typeof map.name !== "string" || map.state !== "setup_pending" && map.state !== "active" || typeof team.name !== "string" || `@${String(team.slug)}` !== input.team || !Array.isArray(delivery.allowedOrigins) || !delivery.allowedOrigins.every((origin) => typeof origin === "string") || delivery.allowedOrigins.length !== input.allowedOrigins.length) {
657
+ return { code: "setup_failed", error: "Map setup returned an invalid response.", ok: false };
658
+ }
659
+ return {
660
+ ok: true,
661
+ value: {
662
+ allowedOrigins: delivery.allowedOrigins,
663
+ map: { id: map.id, name: map.name, state: map.state },
664
+ team: { name: team.name, slug: team.slug }
665
+ }
666
+ };
667
+ }
668
+ var mapSetupFailureMessages = {
669
+ invalid_request: "Tileflow rejected the Map setup request.",
670
+ map_limit: "This Team has reached its Map limit. Archive or delete a Map, or change the plan, in the Tileflow dashboard.",
671
+ permission_required: "Only a Team Admin can create Maps.",
672
+ setup_conflict: "This setup key was already used with another Team or browser access choice. Omit --setup-key to start a new setup.",
673
+ team_not_found: "No Team you administer matches this Team."
674
+ };
675
+ function isMapSetupTeam(value) {
676
+ const team = asRecord(value);
677
+ return typeof team.eligible === "boolean" && typeof team.name === "string" && typeof team.slug === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(team.slug) && (team.unavailableReason === void 0 || team.unavailableReason === "map_limit") && Object.keys(team).every(
678
+ (key) => ["eligible", "name", "slug", "unavailableReason"].includes(key)
679
+ );
680
+ }
681
+ async function publishHostedStyle(api, body, responseLabel, options = {}) {
682
+ const input = body;
683
+ const compactBody = { ...input, artifact: compactTileflowDeploymentArtifact(input.artifact) };
684
+ assertHostedStyleBudget(compactBody);
685
+ const response = await requestHostedJson(
686
+ api.apiUrl,
687
+ "/v1/styles",
688
+ {
689
+ ...jsonRequest("POST", compactBody),
690
+ headers: { ...authorizationHeaders(api.apiKey, api.mapId), "Content-Type": "application/json" }
691
+ },
692
+ options
693
+ );
694
+ if (!response.ok) return { ok: false, status: response.status };
695
+ return {
696
+ ok: true,
697
+ value: parseResponse(response, hostedStyleDeploymentResponseSchema, responseLabel)
698
+ };
699
+ }
700
+ function assertHostedStyleBudget(body) {
701
+ const observedBytes = tileflowDeploymentWebBytes(body);
702
+ const limitBytes = 2e6;
703
+ if (observedBytes > limitBytes)
704
+ throw Object.assign(
705
+ new Error(`Style content is ${observedBytes} bytes; the limit is ${limitBytes} bytes.`),
706
+ { code: "STYLE_DEPLOYMENT_TOO_LARGE", scope: "web", observedBytes, limitBytes }
707
+ );
708
+ }
709
+ async function uploadHostedIconPackage(api, iconPackage, options = {}) {
710
+ const formData = new FormData();
711
+ const fieldNames = {
712
+ "sprite.json": "spriteJson",
713
+ "sprite.png": "spritePng",
714
+ "sprite@2x.json": "sprite2xJson",
715
+ "sprite@2x.png": "sprite2xPng"
716
+ };
717
+ for (const file of iconPackage.files) {
718
+ const fieldName = fieldNames[file.fileName];
719
+ if (!fieldName) throw new Error(`Unknown generated icon package file: ${file.fileName}`);
720
+ const bytes = new Uint8Array(file.source.byteLength);
721
+ bytes.set(file.source);
722
+ formData.append(fieldName, new Blob([bytes.buffer], { type: file.contentType }), file.fileName);
723
+ }
724
+ const response = await requestHostedJson(
725
+ api.apiUrl,
726
+ `/v1/icon-packages/${encodeURIComponent(iconPackage.contentHash)}`,
727
+ { body: formData, headers: authorizationHeaders(api.apiKey, api.mapId), method: "PUT" },
728
+ options
729
+ );
730
+ if (!response.ok) return { ok: false, status: response.status };
731
+ const value = parseResponse(
732
+ response,
733
+ hostedIconPackageResponseSchema,
734
+ "Icon package upload response"
735
+ );
736
+ const publicSpriteUrl = new URL(value.spriteUrl);
737
+ if (value.contentHash !== iconPackage.contentHash || value.iconCount !== iconPackage.manifest.iconNames.length || value.totalBytes !== iconPackage.manifest.files.reduce((total, file) => total + file.byteLength, 0) || publicSpriteUrl.pathname !== `/sprites/${value.id}/sprite` || publicSpriteUrl.search !== "") {
738
+ throw new Error("Icon package upload response did not confirm the submitted package.");
739
+ }
740
+ return { ok: true, value };
741
+ }
742
+ async function uploadHostedFontBundle(api, bundle, options = {}) {
743
+ const formData = new FormData();
744
+ formData.append(
745
+ "manifest",
746
+ new Blob([serializeTileflowFontBundleManifest(bundle.manifest)], {
747
+ type: "application/json"
748
+ }),
749
+ "manifest.json"
750
+ );
751
+ for (const file of bundle.files) {
752
+ const source = typeof file.source === "string" ? new TextEncoder().encode(file.source) : file.source;
753
+ const bytes = new Uint8Array(source.byteLength);
754
+ bytes.set(source);
755
+ formData.append(
756
+ `file:${encodeURIComponent(file.fileName)}`,
757
+ new Blob([bytes.buffer], { type: file.contentType }),
758
+ file.fileName.split("/").at(-1)
759
+ );
760
+ }
761
+ const response = await requestHostedJson(
762
+ api.apiUrl,
763
+ `/v1/font-bundles/${encodeURIComponent(bundle.contentHash)}`,
764
+ { body: formData, headers: authorizationHeaders(api.apiKey, api.mapId), method: "PUT" },
765
+ options
766
+ );
767
+ if (!response.ok) return { ok: false, status: response.status };
768
+ const value = parseResponse(
769
+ response,
770
+ hostedFontBundleResponseSchema,
771
+ "Font bundle upload response"
772
+ );
773
+ const publicBaseUrl = new URL(value.baseUrl);
774
+ if (value.contentHash !== bundle.contentHash || value.fontFaceCount !== bundle.manifest.fontFaces.length || value.totalBytes !== bundle.manifest.files.reduce((total, file) => total + file.byteLength, 0) || publicBaseUrl.pathname !== `/font-bundles/${value.id}` || publicBaseUrl.search !== "") {
775
+ throw new Error("Font bundle upload response did not confirm the submitted bundle.");
776
+ }
777
+ return { ok: true, value };
778
+ }
779
+ async function fetchHostedMapStatus(api, options = {}) {
780
+ const response = await requestHostedJson(
781
+ api.apiUrl,
782
+ "/v1/status",
783
+ { headers: authorizationHeaders(api.apiKey, api.mapId) },
784
+ options
785
+ );
786
+ if (!response.ok) throw new Error(`Status failed: ${response.status}.`);
787
+ return parseResponse(response, hostedMapStatusSchema, "Status response");
788
+ }
789
+ async function revokeHostedAccountSession(apiUrl, accountSession, options = {}) {
790
+ try {
791
+ const response = await requestHostedJson(
792
+ apiUrl,
793
+ "/v1/cli/account/session",
794
+ { headers: authorizationHeaders(accountSession), method: "DELETE" },
795
+ options
796
+ );
797
+ return response.status === 401 || response.ok;
798
+ } catch {
799
+ return false;
800
+ }
801
+ }
802
+ async function withHostedResponse(apiUrl, path, init, options, consume) {
803
+ const origin = normalizeApiOrigin(apiUrl);
804
+ const url = resolveHostedRequestUrl(origin, path);
805
+ const timeoutMs = normalizeTimeout(options.timeoutMs);
806
+ const requestSignal = createRequestSignal(options.signal ?? init.signal ?? void 0, timeoutMs);
807
+ const requestFetch = options.fetch ?? globalThis.fetch;
808
+ const { signal: _ignoredSignal, ...requestInit } = init;
809
+ try {
810
+ const operation = Promise.resolve().then(
811
+ () => requestFetch(url, { ...requestInit, redirect: "error", signal: requestSignal.signal })
812
+ ).then(consume);
813
+ return await raceWithAbort(operation, requestSignal.signal);
814
+ } finally {
815
+ requestSignal.cleanup();
816
+ }
817
+ }
818
+ function raceWithAbort(operation, signal) {
819
+ if (signal.aborted) return Promise.reject(signal.reason);
820
+ return new Promise((resolve2, reject) => {
821
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
822
+ const onAbort = () => {
823
+ cleanup();
824
+ reject(signal.reason);
825
+ };
826
+ signal.addEventListener("abort", onAbort, { once: true });
827
+ operation.then(
828
+ (value) => {
829
+ cleanup();
830
+ resolve2(value);
831
+ },
832
+ (error) => {
833
+ cleanup();
834
+ reject(error);
835
+ }
836
+ );
837
+ });
838
+ }
839
+ function resolveHostedRequestUrl(origin, path) {
840
+ if (!path.startsWith("/") || path.startsWith("//") || /[\p{Cc}\\]/u.test(path)) {
841
+ throw new TypeError("Hosted API path must be a safe root-relative path.");
842
+ }
843
+ const url = new URL(path, origin);
844
+ if (url.origin !== origin || url.username || url.password || url.hash) {
845
+ throw new TypeError("Hosted API path must stay on the configured API origin.");
846
+ }
847
+ return url.toString();
848
+ }
849
+ function normalizeTimeout(value) {
850
+ if (value === void 0) return defaultHostedRequestTimeoutMs;
851
+ if (!Number.isFinite(value) || value < 1 || value > maximumHostedRequestTimeoutMs) {
852
+ throw new TypeError("Hosted request timeoutMs must be between 1 and 60000 milliseconds.");
853
+ }
854
+ return value;
855
+ }
856
+ function createRequestSignal(external, timeoutMs) {
857
+ const controller = new AbortController();
858
+ const abortFromExternal = () => controller.abort(external?.reason);
859
+ if (external?.aborted) abortFromExternal();
860
+ else external?.addEventListener("abort", abortFromExternal, { once: true });
861
+ const timeout = setTimeout(
862
+ () => controller.abort(new DOMException("Hosted request timed out", "TimeoutError")),
863
+ timeoutMs
864
+ );
865
+ return {
866
+ cleanup() {
867
+ clearTimeout(timeout);
868
+ external?.removeEventListener("abort", abortFromExternal);
869
+ },
870
+ signal: controller.signal
871
+ };
872
+ }
873
+ function jsonRequest(method, body) {
874
+ return {
875
+ body: JSON.stringify(body),
876
+ headers: { "Content-Type": "application/json" },
877
+ method
878
+ };
879
+ }
880
+ function authorizationHeaders(token, mapId) {
881
+ if (!token || token.length > 8192 || /[\p{Cc}]/u.test(token)) {
882
+ throw new TypeError("Hosted authorization credential is invalid.");
883
+ }
884
+ if (mapId !== void 0 && !/^map_[A-Za-z0-9_-]{16}$/u.test(mapId))
885
+ throw new TypeError("Hosted Map ID is invalid.");
886
+ return {
887
+ Authorization: `Bearer ${token}`,
888
+ ...mapId === void 0 ? {} : { "X-Tileflow-Map-Id": mapId }
889
+ };
890
+ }
891
+ function parseResponse(response, schema, label) {
892
+ if (!response.json) throw new Error(`${label} returned invalid JSON.`);
893
+ const parsed = schema.safeParse(response.body);
894
+ if (!parsed.success) throw new Error(`${label} returned an invalid response.`);
895
+ return parsed.data;
896
+ }
897
+ function isDeviceAuthorization(value) {
898
+ const authorization = asRecord(value);
899
+ return typeof authorization.apiUrl === "string" && safeOrigin(authorization.apiUrl) !== null && safeIdentifier(authorization.deviceCode, 512) && Number.isInteger(authorization.expiresIn) && authorization.expiresIn >= 1 && authorization.expiresIn <= 3600 && Number.isInteger(authorization.interval) && authorization.interval >= 1 && authorization.interval <= 60 && safeIdentifier(authorization.userCode, 64) && safePublicUrl(authorization.verificationUri) && safePublicUrl(authorization.verificationUriComplete);
900
+ }
901
+ function isDeviceToken(value) {
902
+ const token = asRecord(value);
903
+ return isAccountIdentity(token.account) && typeof token.accountSession === "string" && /^tf_session_[0-9a-f]{64}$/u.test(token.accountSession) && typeof token.apiUrl === "string" && safeOrigin(token.apiUrl) !== null && validIsoDate(token.createdAt) && validIsoDate(token.expiresAt) && Date.parse(token.expiresAt) > Date.parse(token.createdAt) && safeIdentifier(token.sessionId, 200) && !Object.hasOwn(token, "project");
904
+ }
905
+ function isProjectIdentity(value) {
906
+ const identity = asRecord(value);
907
+ return safeIdentifier(identity.id, 160) && safeText(identity.name, 200) && typeof identity.slug === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(identity.slug);
908
+ }
909
+ function isAccountIdentity(value) {
910
+ const account = asRecord(value);
911
+ return safeIdentifier(account.id, 200) && safeText(account.name, 200) && typeof account.email === "string" && account.email.length > 2 && account.email.length <= 320 && account.email.includes("@") && !/[\p{Cc}]/u.test(account.email);
912
+ }
913
+ function safeIdentifier(value, maximum) {
914
+ return typeof value === "string" && value.length > 0 && value.length <= maximum && !/[\p{Cc}]/u.test(value);
915
+ }
916
+ function safeText(value, maximum) {
917
+ return safeIdentifier(value, maximum);
918
+ }
919
+ function safePublicUrl(value) {
920
+ if (typeof value !== "string" || value.length > 2048) return false;
921
+ try {
922
+ const url = new URL(value);
923
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && !url.hash;
924
+ } catch {
925
+ return false;
926
+ }
927
+ }
928
+ function safeOrigin(value) {
929
+ try {
930
+ return normalizeApiOrigin(value);
931
+ } catch {
932
+ return null;
933
+ }
934
+ }
935
+ function validIsoDate(value) {
936
+ return typeof value === "string" && value.length <= 64 && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(value) && Number.isFinite(Date.parse(value));
937
+ }
938
+ function asRecord(value) {
939
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
940
+ }
941
+ function safeTransportError(error, fallback) {
942
+ if (error instanceof Error && /safe size limit|timed out/u.test(error.message)) {
943
+ return error.message;
944
+ }
945
+ return fallback;
946
+ }
947
+
948
+ // src/tileset-command.ts
949
+ import pc3 from "picocolors";
950
+ import { z as z4 } from "zod";
951
+ import { serializeCanonicalJson as serializeCanonicalJson3 } from "@tileflow/core";
952
+ import { inspectTileflowPmtiles } from "@tileflow/dev/tilesets";
953
+
954
+ // src/hosted-team.ts
955
+ import pc2 from "picocolors";
956
+ import { serializeCanonicalJson } from "@tileflow/core";
957
+ async function resolveTeamAuthority(options, scopes, dependencies) {
958
+ let apiOrigin;
959
+ try {
960
+ apiOrigin = normalizeApiOrigin(options.apiUrl ?? dependencies.defaultApiUrl);
961
+ } catch (error) {
962
+ emitFailure(options.json, "invalid_api_url", safeMessage(error));
963
+ return null;
964
+ }
965
+ if (options.apiKey !== void 0) {
966
+ if (!isHostedApiKey(options.apiKey)) {
967
+ emitFailure(options.json, "invalid_team_data_key", "Team data API key is invalid.");
968
+ return null;
969
+ }
970
+ if (options.team) {
971
+ emitFailure(
972
+ options.json,
973
+ "team_selector_with_api_key",
974
+ "A Team data key already selects its Team; omit --team."
975
+ );
976
+ return null;
977
+ }
978
+ return { apiOrigin, credential: options.apiKey, team: null };
979
+ }
980
+ if (!allowsStoredAccountSession()) {
981
+ emitFailure(
982
+ options.json,
983
+ "api_key_required",
984
+ "CI requires TILEFLOW_API_KEY or --api-key. Saved personal logins are not used in CI."
985
+ );
986
+ return null;
987
+ }
988
+ let config;
989
+ try {
990
+ config = await dependencies.loadAuthConfig();
991
+ } catch (error) {
992
+ emitFailure(options.json, "auth_state_unavailable", safeMessage(error));
993
+ return null;
994
+ }
995
+ const selected = resolveAccountSession(config, options.apiUrl ?? dependencies.defaultApiUrl);
996
+ if (selected.kind !== "selected") {
997
+ emitFailure(
998
+ options.json,
999
+ `account_session_${selected.kind}`,
1000
+ selected.kind === "expired" ? "Account session expired. Run tileflow login." : "Run tileflow login."
1001
+ );
1002
+ return null;
1003
+ }
1004
+ const discovered = await listAccountTeams(selected.session);
1005
+ if (!discovered.ok) {
1006
+ emitFailure(options.json, "team_discovery_failed", discovered.error);
1007
+ return null;
1008
+ }
1009
+ const requested = options.team ? /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u.exec(options.team) : null;
1010
+ if (options.team && !requested) {
1011
+ emitFailure(options.json, "invalid_team", "Team must use @team syntax.");
1012
+ return null;
1013
+ }
1014
+ const team = requested ? discovered.teams.find(({ slug }) => slug === requested[1]) : discovered.teams.length === 1 ? discovered.teams[0] : null;
1015
+ if (!team) {
1016
+ const options_ = discovered.teams.map(({ slug }) => `@${slug}`);
1017
+ emitFailure(
1018
+ options.json,
1019
+ options_.length > 1 ? "team_ambiguous" : "team_not_found",
1020
+ options_.length > 1 ? `Choose one Team with --team: ${options_.join(", ")}.` : "No matching Team is available.",
1021
+ { options: options_ }
1022
+ );
1023
+ return null;
1024
+ }
1025
+ const capability = await requestTeamCapability(selected.session, `@${team.slug}`, scopes);
1026
+ if (!capability.ok || capability.team.id !== team.id) {
1027
+ emitFailure(
1028
+ options.json,
1029
+ "team_capability_failed",
1030
+ capability.ok ? "Team capability response was invalid." : capability.error
1031
+ );
1032
+ return null;
1033
+ }
1034
+ let current = capability;
1035
+ let renewal;
1036
+ const getCredential = async () => {
1037
+ if (Date.parse(current.expiresAt) > Date.now() + 3e4) return current.capability;
1038
+ renewal ??= requestTeamCapability(selected.session, `@${team.slug}`, scopes).then((next) => {
1039
+ if (!next.ok || next.team.id !== team.id || Date.parse(next.expiresAt) <= Date.now() + 3e4) {
1040
+ throw new Error("Team authorization could not be renewed. Run tileflow login.");
1041
+ }
1042
+ current = next;
1043
+ }).finally(() => {
1044
+ renewal = void 0;
1045
+ });
1046
+ await renewal;
1047
+ return current.capability;
1048
+ };
1049
+ return {
1050
+ apiOrigin: selected.session.apiOrigin,
1051
+ credential: capability.capability,
1052
+ getCredential,
1053
+ team
1054
+ };
1055
+ }
1056
+ async function authorizedTeamRequest(authority, path, method, init = {}) {
1057
+ const credential = await authority.getCredential?.() ?? authority.credential;
1058
+ return requestHostedJson(authority.apiOrigin, path, {
1059
+ ...init.body === void 0 ? {} : { body: init.body },
1060
+ headers: { Authorization: `Bearer ${credential}`, ...init.headers },
1061
+ method
1062
+ });
1063
+ }
1064
+ function emitJson(value) {
1065
+ process.stdout.write(`${serializeCanonicalJson(value)}
1066
+ `);
1067
+ }
1068
+ function emitFailure(json, code, message, context = {}) {
1069
+ if (json) {
1070
+ process.stderr.write(
1071
+ `${serializeCanonicalJson({ error: { code, ...context, message }, ok: false, schemaVersion: 1 })}
1072
+ `
1073
+ );
1074
+ } else {
1075
+ console.error(`${pc2.red("Error:")} ${message}`);
1076
+ }
1077
+ process.exitCode = 1;
1078
+ }
1079
+ function safeMessage(error) {
1080
+ return error instanceof Error ? error.message : "Team command failed.";
1081
+ }
1082
+
1083
+ // src/tileset-upload.ts
1084
+ import { createHash } from "crypto";
1085
+ import { open as open2, stat } from "fs/promises";
1086
+ import { z as z3 } from "zod";
1087
+ import { serializeCanonicalJson as serializeCanonicalJson2 } from "@tileflow/core";
1088
+ var teamTilesetPartSize = 16777216;
1089
+ var maximumTeamTilesetBytes = 4e9;
1090
+ var authorizationBatchSize = 16;
1091
+ var uploadConcurrency = 4;
1092
+ var maximumPartAttempts = 4;
1093
+ var maximumControlAttempts = 4;
1094
+ var maximumInitiatingWaitMs = 2 * 60 * 1e3;
1095
+ var partTransferTimeoutMs = 10 * 60 * 1e3;
1096
+ var authorizationClockSkewMs = 3e4;
1097
+ var minimumPartAuthorizationLifetimeMs = partTransferTimeoutMs + authorizationClockSkewMs;
1098
+ var TeamTilesetUploadError = class extends Error {
1099
+ code;
1100
+ details;
1101
+ constructor(code, message, details = {}) {
1102
+ super(message);
1103
+ this.name = "TeamTilesetUploadError";
1104
+ this.code = code;
1105
+ this.details = details;
1106
+ }
1107
+ };
1108
+ function createTeamTilesetPartLayout(byteCount) {
1109
+ if (!Number.isSafeInteger(byteCount) || byteCount < 1 || byteCount > maximumTeamTilesetBytes) {
1110
+ throw new TeamTilesetUploadError(
1111
+ "TF_TILESET_VERSION_TOO_LARGE",
1112
+ "PMTiles archive exceeds the 4,000,000,000-byte platform maximum.",
1113
+ {
1114
+ actualBytes: Number.isSafeInteger(byteCount) && byteCount >= 0 ? byteCount : 0,
1115
+ limitBytes: maximumTeamTilesetBytes
1116
+ }
1117
+ );
1118
+ }
1119
+ return Object.freeze(
1120
+ Array.from({ length: Math.ceil(byteCount / teamTilesetPartSize) }, (_, index) => {
1121
+ const position = index * teamTilesetPartSize;
1122
+ return Object.freeze({
1123
+ byteCount: Math.min(teamTilesetPartSize, byteCount - position),
1124
+ partNumber: index + 1,
1125
+ position
1126
+ });
1127
+ })
1128
+ );
1129
+ }
1130
+ async function inspectNodeTeamTilesetArchive(path) {
1131
+ let value;
1132
+ try {
1133
+ value = await stat(path, { bigint: true });
1134
+ } catch {
1135
+ throw new TeamTilesetUploadError(
1136
+ "TF_TILESET_ARCHIVE_INVALID",
1137
+ "PMTiles archive is unavailable."
1138
+ );
1139
+ }
1140
+ if (!value.isFile()) {
1141
+ throw new TeamTilesetUploadError(
1142
+ "TF_TILESET_ARCHIVE_INVALID",
1143
+ "PMTiles archive must be a file."
1144
+ );
1145
+ }
1146
+ return checkedIdentity(value.dev, value.ino, value.mtimeNs, value.size);
1147
+ }
1148
+ async function openNodeTeamTilesetArchive(path, expectedIdentity) {
1149
+ const handle = await open2(path, "r");
1150
+ const identity = async () => {
1151
+ let pathStat;
1152
+ let handleStat;
1153
+ try {
1154
+ [pathStat, handleStat] = await Promise.all([
1155
+ stat(path, { bigint: true }),
1156
+ handle.stat({ bigint: true })
1157
+ ]);
1158
+ } catch {
1159
+ throw localFileChanged();
1160
+ }
1161
+ if (!pathStat.isFile() || !handleStat.isFile()) throw localFileChanged();
1162
+ const pathIdentity = checkedIdentity(
1163
+ pathStat.dev,
1164
+ pathStat.ino,
1165
+ pathStat.mtimeNs,
1166
+ pathStat.size
1167
+ );
1168
+ const handleIdentity = checkedIdentity(
1169
+ handleStat.dev,
1170
+ handleStat.ino,
1171
+ handleStat.mtimeNs,
1172
+ handleStat.size
1173
+ );
1174
+ assertFileIdentity(pathIdentity, handleIdentity);
1175
+ return pathIdentity;
1176
+ };
1177
+ try {
1178
+ assertFileIdentity(expectedIdentity, await identity());
1179
+ } catch (error) {
1180
+ await handle.close();
1181
+ throw error;
1182
+ }
1183
+ const readInto = async (position, target) => {
1184
+ let offset = 0;
1185
+ while (offset < target.byteLength) {
1186
+ const result = await handle.read(
1187
+ target,
1188
+ offset,
1189
+ target.byteLength - offset,
1190
+ position + offset
1191
+ );
1192
+ if (result.bytesRead === 0) throw localFileChanged();
1193
+ offset += result.bytesRead;
1194
+ }
1195
+ return offset;
1196
+ };
1197
+ return {
1198
+ close: () => handle.close(),
1199
+ identity,
1200
+ async read(position, byteCount) {
1201
+ const bytes = Buffer.allocUnsafe(byteCount);
1202
+ await readInto(position, bytes);
1203
+ return bytes;
1204
+ },
1205
+ readInto
1206
+ };
1207
+ }
1208
+ async function prepareTeamTilesetUpload(file) {
1209
+ const identity = await file.identity();
1210
+ const layout = createTeamTilesetPartLayout(identity.size);
1211
+ const parts = [];
1212
+ const reusable = file.readInto ? Buffer.allocUnsafe(Math.min(identity.size, teamTilesetPartSize)) : void 0;
1213
+ for (const part of layout) {
1214
+ const bytes = reusable ? reusable.subarray(0, part.byteCount) : await file.read(part.position, part.byteCount);
1215
+ if (reusable && await file.readInto(part.position, bytes) !== part.byteCount) {
1216
+ throw localFileChanged();
1217
+ }
1218
+ if (bytes.byteLength !== part.byteCount) throw localFileChanged();
1219
+ parts.push(
1220
+ Object.freeze({
1221
+ byteCount: part.byteCount,
1222
+ md5Base64: createHash("md5").update(bytes).digest("base64"),
1223
+ partNumber: part.partNumber,
1224
+ sha256Base64: createHash("sha256").update(bytes).digest("base64")
1225
+ })
1226
+ );
1227
+ }
1228
+ assertFileIdentity(identity, await file.identity());
1229
+ const contentIdentity = {
1230
+ algorithm: "sha256-fixed-parts-v1",
1231
+ byteCount: identity.size,
1232
+ partSize: teamTilesetPartSize,
1233
+ parts: parts.map(({ byteCount, partNumber, sha256Base64 }) => ({
1234
+ byteCount,
1235
+ partNumber,
1236
+ sha256Base64
1237
+ }))
1238
+ };
1239
+ return Object.freeze({
1240
+ byteCount: identity.size,
1241
+ contentHash: createHash("sha256").update(serializeCanonicalJson2(contentIdentity)).digest("hex"),
1242
+ contentHashAlgorithm: "sha256-fixed-parts-v1",
1243
+ identity,
1244
+ partSize: teamTilesetPartSize,
1245
+ parts: Object.freeze(parts),
1246
+ schemaVersion: 1
1247
+ });
1248
+ }
1249
+ function createTeamTilesetUploadIdempotencyKey(tilesetId, upload) {
1250
+ if (!/^tls_[A-Za-z0-9_-]{16,76}$/u.test(tilesetId)) {
1251
+ throw new TeamTilesetUploadError(
1252
+ "TF_TILESET_NOT_FOUND",
1253
+ "Tileset registration returned an invalid ID."
1254
+ );
1255
+ }
1256
+ return createHash("sha256").update(
1257
+ serializeCanonicalJson2({
1258
+ byteCount: upload.byteCount,
1259
+ contentHash: upload.contentHash,
1260
+ contentHashAlgorithm: upload.contentHashAlgorithm,
1261
+ tilesetId
1262
+ })
1263
+ ).digest("hex");
1264
+ }
1265
+ async function publishPreparedTeamTilesetUpload(input) {
1266
+ assertFileIdentity(input.upload.identity, await input.file.identity());
1267
+ const beginInput = {
1268
+ byteCount: input.upload.byteCount,
1269
+ contentHash: input.upload.contentHash,
1270
+ contentHashAlgorithm: input.upload.contentHashAlgorithm,
1271
+ idempotencyKey: createTeamTilesetUploadIdempotencyKey(input.tilesetId, input.upload),
1272
+ parts: input.upload.parts,
1273
+ partSize: input.upload.partSize,
1274
+ schemaVersion: 1
1275
+ };
1276
+ const begun = await input.transport.begin(beginInput);
1277
+ assertSessionMatches(begun, input.upload);
1278
+ const changed = begun.state !== "published";
1279
+ if (begun.state === "published") return publicationResult(begun, input.upload, changed);
1280
+ let current = await input.transport.status(begun.uploadId);
1281
+ assertSessionMatches(current, input.upload, true);
1282
+ assertUsableSession(current);
1283
+ let initiatingWaitMs = 0;
1284
+ for (let attempt = 1; current.state === "initiating"; attempt += 1) {
1285
+ const delay = Math.min(
1286
+ 2e3,
1287
+ 250 * 2 ** Math.min(attempt - 1, 3),
1288
+ maximumInitiatingWaitMs - initiatingWaitMs
1289
+ );
1290
+ if (delay <= 0) {
1291
+ throw new TeamTilesetUploadError(
1292
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1293
+ "Tileset upload initialization did not converge within two minutes."
1294
+ );
1295
+ }
1296
+ await (input.sleep ?? defaultSleep)(delay);
1297
+ initiatingWaitMs += delay;
1298
+ current = await input.transport.status(current.uploadId);
1299
+ assertSessionMatches(current, input.upload, true);
1300
+ assertUsableSession(current);
1301
+ }
1302
+ if (current.state === "uploading") {
1303
+ const uploading = current;
1304
+ const missingParts = input.upload.parts.filter(
1305
+ (part) => !uploading.parts.find((candidate) => candidate.partNumber === part.partNumber)?.uploaded
1306
+ );
1307
+ await uploadMissingParts({ ...input, missingParts, uploadId: uploading.uploadId });
1308
+ assertFileIdentity(input.upload.identity, await input.file.identity());
1309
+ current = await input.transport.complete(uploading.uploadId);
1310
+ assertSessionMatches(current, input.upload);
1311
+ assertUsableSession(current);
1312
+ }
1313
+ while (current.state !== "published") {
1314
+ if (!["publishing", "ready_to_publish", "validating"].includes(current.state)) {
1315
+ throw new TeamTilesetUploadError(
1316
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1317
+ "Tileset upload cannot continue from its current state."
1318
+ );
1319
+ }
1320
+ input.onProgress?.({
1321
+ completedParts: input.upload.parts.length,
1322
+ phase: "validating",
1323
+ totalParts: input.upload.parts.length
1324
+ });
1325
+ await (input.sleep ?? defaultSleep)(2e3);
1326
+ current = await input.transport.status(current.uploadId);
1327
+ assertSessionMatches(current, input.upload, true);
1328
+ assertUsableSession(current);
1329
+ }
1330
+ return publicationResult(current, input.upload, changed);
1331
+ }
1332
+ function createHostedTeamTilesetUploadTransport(input) {
1333
+ const basePath = `/v1/tilesets/${encodeURIComponent(input.tilesetId)}/uploads`;
1334
+ const control = async (path, init) => {
1335
+ for (let attempt = 1; attempt <= maximumControlAttempts; attempt += 1) {
1336
+ let credential;
1337
+ try {
1338
+ credential = await input.getCredential?.() ?? input.credential;
1339
+ } catch {
1340
+ throw new TeamTilesetUploadError(
1341
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1342
+ "Team authorization could not be renewed. Run tileflow login and retry the upload."
1343
+ );
1344
+ }
1345
+ let response;
1346
+ try {
1347
+ response = await requestHostedJson(
1348
+ input.apiOrigin,
1349
+ path,
1350
+ {
1351
+ ...init,
1352
+ headers: {
1353
+ Authorization: `Bearer ${credential}`,
1354
+ ...init.body ? { "Content-Type": "application/json" } : {},
1355
+ ...init.headers
1356
+ }
1357
+ },
1358
+ { fetch: input.fetch }
1359
+ );
1360
+ } catch {
1361
+ if (attempt === maximumControlAttempts) {
1362
+ throw new TeamTilesetUploadError(
1363
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1364
+ "Tileset upload control request did not complete."
1365
+ );
1366
+ }
1367
+ }
1368
+ if (response?.ok) return response.body;
1369
+ if (response && (asRecord2(response.body).code === "TF_TILESET_UPLOAD_DISABLED" || !isRetryableStatus(response.status))) {
1370
+ throw responseError(response);
1371
+ }
1372
+ if (attempt === maximumControlAttempts) {
1373
+ if (response) throw responseError(response);
1374
+ break;
1375
+ }
1376
+ await (input.sleep ?? defaultSleep)(Math.min(2e3, 250 * 2 ** (attempt - 1)));
1377
+ }
1378
+ throw new TeamTilesetUploadError(
1379
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1380
+ "Tileset upload control request did not complete."
1381
+ );
1382
+ };
1383
+ return {
1384
+ async authorize(uploadId, partNumbers) {
1385
+ const body = await control(
1386
+ `${basePath}/${encodeURIComponent(uploadId)}/part-authorizations`,
1387
+ {
1388
+ body: JSON.stringify({ partNumbers, schemaVersion: 1 }),
1389
+ method: "POST"
1390
+ }
1391
+ );
1392
+ return authorizationResponseSchema.parse(body).authorizations;
1393
+ },
1394
+ async begin(body) {
1395
+ return parseUploadSession(
1396
+ await control(basePath, { body: JSON.stringify(body), method: "POST" })
1397
+ );
1398
+ },
1399
+ async complete(uploadId) {
1400
+ return parseUploadSession(
1401
+ await control(`${basePath}/${encodeURIComponent(uploadId)}/complete`, {
1402
+ body: JSON.stringify({ schemaVersion: 1 }),
1403
+ method: "POST"
1404
+ })
1405
+ );
1406
+ },
1407
+ async putPart(authorization, bytes) {
1408
+ const controller = new AbortController();
1409
+ try {
1410
+ const body = new ReadableStream({
1411
+ start(controller2) {
1412
+ controller2.enqueue(bytes);
1413
+ controller2.close();
1414
+ }
1415
+ });
1416
+ const response = await (input.fetch ?? globalThis.fetch)(authorization.url, {
1417
+ body,
1418
+ duplex: "half",
1419
+ headers: authorization.headers,
1420
+ method: "PUT",
1421
+ redirect: "error",
1422
+ signal: AbortSignal.any([controller.signal, AbortSignal.timeout(partTransferTimeoutMs)])
1423
+ });
1424
+ const etag = response.headers.get("etag");
1425
+ await response.body?.cancel().catch(() => void 0);
1426
+ return { etag, status: response.status };
1427
+ } catch {
1428
+ throw new Error("Direct part transfer failed.");
1429
+ } finally {
1430
+ controller.abort();
1431
+ }
1432
+ },
1433
+ async recordPart(uploadId, body) {
1434
+ partReceiptResponseSchema.parse(
1435
+ await control(`${basePath}/${encodeURIComponent(uploadId)}/parts`, {
1436
+ body: JSON.stringify(body),
1437
+ method: "POST"
1438
+ })
1439
+ );
1440
+ },
1441
+ async status(uploadId) {
1442
+ return parseUploadSession(
1443
+ await control(`${basePath}/${encodeURIComponent(uploadId)}`, { method: "GET" })
1444
+ );
1445
+ }
1446
+ };
1447
+ }
1448
+ async function uploadMissingParts(input) {
1449
+ let completedParts = input.upload.parts.length - input.missingParts.length;
1450
+ const transferBufferBytes = Math.max(0, ...input.missingParts.map((part) => part.byteCount));
1451
+ const transferBuffers = input.file.readInto ? Array.from(
1452
+ { length: Math.min(uploadConcurrency, input.missingParts.length) },
1453
+ () => Buffer.allocUnsafe(transferBufferBytes)
1454
+ ) : [];
1455
+ for (let offset = 0; offset < input.missingParts.length; offset += authorizationBatchSize) {
1456
+ const authorizationParts = input.missingParts.slice(offset, offset + authorizationBatchSize);
1457
+ const initial = indexAuthorizations(
1458
+ await input.transport.authorize(
1459
+ input.uploadId,
1460
+ authorizationParts.map(({ partNumber }) => partNumber)
1461
+ ),
1462
+ authorizationParts
1463
+ );
1464
+ for (let groupOffset = 0; groupOffset < authorizationParts.length; groupOffset += uploadConcurrency) {
1465
+ assertFileIdentity(input.upload.identity, await input.file.identity());
1466
+ const group = authorizationParts.slice(groupOffset, groupOffset + uploadConcurrency);
1467
+ const settled = await Promise.allSettled(
1468
+ group.map(async (part, index) => {
1469
+ const position = (part.partNumber - 1) * teamTilesetPartSize;
1470
+ const reusable = transferBuffers[index];
1471
+ const bytes = reusable ? reusable.subarray(0, part.byteCount) : await input.file.read(position, part.byteCount);
1472
+ if (reusable && await input.file.readInto(position, bytes) !== part.byteCount) {
1473
+ throw localFileChanged();
1474
+ }
1475
+ if (bytes.byteLength !== part.byteCount) throw localFileChanged();
1476
+ await uploadPartWithRetry({
1477
+ ...input,
1478
+ initialAuthorization: initial.get(part.partNumber),
1479
+ part,
1480
+ bytes
1481
+ });
1482
+ })
1483
+ );
1484
+ const failed = settled.find(
1485
+ (result) => result.status === "rejected"
1486
+ );
1487
+ if (failed) throw failed.reason;
1488
+ completedParts += group.length;
1489
+ input.onProgress?.({
1490
+ completedParts,
1491
+ phase: "uploading",
1492
+ totalParts: input.upload.parts.length
1493
+ });
1494
+ }
1495
+ }
1496
+ }
1497
+ async function uploadPartWithRetry(input) {
1498
+ let authorization = input.initialAuthorization;
1499
+ for (let attempt = 1; attempt <= maximumPartAttempts; attempt += 1) {
1500
+ if (!authorizationCoversTransfer(authorization, input.now?.() ?? Date.now())) {
1501
+ const refreshed2 = await input.transport.authorize(input.uploadId, [input.part.partNumber]);
1502
+ authorization = indexAuthorizations(refreshed2, [input.part]).get(input.part.partNumber);
1503
+ if (!authorizationCoversTransfer(authorization, input.now?.() ?? Date.now())) {
1504
+ throw invalidRemoteResponse();
1505
+ }
1506
+ }
1507
+ assertAuthorizationMatches(authorization, input.part);
1508
+ let result;
1509
+ try {
1510
+ result = await input.transport.putPart(authorization, input.bytes);
1511
+ } catch {
1512
+ result = { etag: null, status: 503 };
1513
+ }
1514
+ if (result.status >= 200 && result.status < 300) {
1515
+ const etag = normalizePartEtag(result.etag, input.part.md5Base64);
1516
+ await input.transport.recordPart(input.uploadId, {
1517
+ etag,
1518
+ partNumber: input.part.partNumber
1519
+ });
1520
+ return;
1521
+ }
1522
+ if ((result.status === 401 || result.status === 403) && !authorizationCoversTransfer(authorization, input.now?.() ?? Date.now()) && attempt < maximumPartAttempts) {
1523
+ const refreshed2 = await input.transport.authorize(input.uploadId, [input.part.partNumber]);
1524
+ authorization = indexAuthorizations(refreshed2, [input.part]).get(input.part.partNumber);
1525
+ continue;
1526
+ }
1527
+ if (!isRetryableStatus(result.status)) {
1528
+ throw new TeamTilesetUploadError(
1529
+ result.status === 401 || result.status === 403 ? "TF_TILESET_UPLOAD_AUTHORIZATION_FAILED" : "TF_TILESET_UPLOAD_PART_INVALID",
1530
+ "Direct part transfer was rejected."
1531
+ );
1532
+ }
1533
+ if (attempt === maximumPartAttempts) break;
1534
+ await (input.sleep ?? defaultSleep)(Math.min(2e3, 250 * 2 ** (attempt - 1)));
1535
+ const refreshed = await input.transport.authorize(input.uploadId, [input.part.partNumber]);
1536
+ authorization = indexAuthorizations(refreshed, [input.part]).get(input.part.partNumber);
1537
+ }
1538
+ throw new TeamTilesetUploadError(
1539
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1540
+ "Direct part transfer did not complete after bounded retries."
1541
+ );
1542
+ }
1543
+ function authorizationCoversTransfer(authorization, now) {
1544
+ return Date.parse(authorization.expiresAt) - now >= minimumPartAuthorizationLifetimeMs;
1545
+ }
1546
+ function isRetryableStatus(status) {
1547
+ return status === 408 || status === 429 || status >= 500;
1548
+ }
1549
+ function indexAuthorizations(authorizations, parts) {
1550
+ const expected = new Map(parts.map((part) => [part.partNumber, part]));
1551
+ const indexed = /* @__PURE__ */ new Map();
1552
+ for (const authorization of authorizations) {
1553
+ const part = expected.get(authorization.partNumber);
1554
+ if (!part || indexed.has(authorization.partNumber)) {
1555
+ throw invalidRemoteResponse();
1556
+ }
1557
+ assertAuthorizationMatches(authorization, part);
1558
+ indexed.set(authorization.partNumber, authorization);
1559
+ }
1560
+ if (indexed.size !== expected.size) throw invalidRemoteResponse();
1561
+ return indexed;
1562
+ }
1563
+ function assertAuthorizationMatches(authorization, part) {
1564
+ if (authorization.partNumber !== part.partNumber || authorization.method !== "PUT" || authorization.headers["Content-Length"] !== String(part.byteCount) || authorization.headers["Content-MD5"] !== part.md5Base64) {
1565
+ throw invalidRemoteResponse();
1566
+ }
1567
+ }
1568
+ function normalizePartEtag(value, md5Base64) {
1569
+ if (!value || !/^(?:[0-9a-f]{32}|"[0-9a-f]{32}")$/u.test(value)) {
1570
+ throw new TeamTilesetUploadError(
1571
+ "TF_TILESET_UPLOAD_PART_INVALID",
1572
+ "Direct part transfer returned an invalid ETag."
1573
+ );
1574
+ }
1575
+ const normalized = value.replaceAll('"', "");
1576
+ if (normalized !== Buffer.from(md5Base64, "base64").toString("hex")) {
1577
+ throw new TeamTilesetUploadError(
1578
+ "TF_TILESET_UPLOAD_PART_INVALID",
1579
+ "Direct part transfer returned a conflicting ETag."
1580
+ );
1581
+ }
1582
+ return normalized;
1583
+ }
1584
+ function assertSessionMatches(session, upload, requireParts = false) {
1585
+ if (session.byteCount !== upload.byteCount || session.contentHash !== upload.contentHash || session.contentHashAlgorithm !== upload.contentHashAlgorithm) {
1586
+ throw invalidRemoteResponse();
1587
+ }
1588
+ if (session.state === "published") return;
1589
+ if (session.partCount !== upload.parts.length || session.partSize !== upload.partSize || requireParts && session.parts.length !== upload.parts.length || session.parts.some((part, index) => {
1590
+ const expected = upload.parts[index];
1591
+ return !expected || part.partNumber !== expected.partNumber || part.byteCount !== expected.byteCount;
1592
+ })) {
1593
+ throw invalidRemoteResponse();
1594
+ }
1595
+ }
1596
+ function assertUsableSession(session) {
1597
+ if (session.state === "published") return;
1598
+ if (session.failure) {
1599
+ throw new TeamTilesetUploadError(session.failure.code, failureMessage(session.failure.code));
1600
+ }
1601
+ if (["aborted", "aborting", "expired", "expiring", "rejected", "rejecting"].includes(session.state)) {
1602
+ throw new TeamTilesetUploadError(
1603
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1604
+ "Tileset upload is no longer resumable."
1605
+ );
1606
+ }
1607
+ }
1608
+ function publicationResult(session, upload, changed) {
1609
+ return Object.freeze({
1610
+ byteCount: upload.byteCount,
1611
+ changed,
1612
+ contentHash: upload.contentHash,
1613
+ contentHashAlgorithm: upload.contentHashAlgorithm,
1614
+ state: "published",
1615
+ version: session.publishedVersion,
1616
+ versionId: session.versionId
1617
+ });
1618
+ }
1619
+ function assertFileIdentity(expected, actual) {
1620
+ const inodeChanged = expected.inode !== 0n && actual.inode !== 0n && (expected.inode !== actual.inode || expected.device !== actual.device);
1621
+ if (inodeChanged || expected.size !== actual.size || expected.mtimeNanoseconds !== actual.mtimeNanoseconds) {
1622
+ throw localFileChanged();
1623
+ }
1624
+ }
1625
+ function checkedIdentity(device, inode, mtimeNanoseconds, size) {
1626
+ const numericSize = Number(size);
1627
+ if (numericSize === 0) {
1628
+ throw new TeamTilesetUploadError("TF_TILESET_ARCHIVE_EMPTY", "PMTiles archive is empty.");
1629
+ }
1630
+ if (numericSize > maximumTeamTilesetBytes) {
1631
+ throw new TeamTilesetUploadError(
1632
+ "TF_TILESET_VERSION_TOO_LARGE",
1633
+ "PMTiles archive exceeds the 4,000,000,000-byte platform maximum.",
1634
+ { actualBytes: numericSize, limitBytes: maximumTeamTilesetBytes }
1635
+ );
1636
+ }
1637
+ return Object.freeze({ device, inode, mtimeNanoseconds, size: numericSize });
1638
+ }
1639
+ function localFileChanged() {
1640
+ return new TeamTilesetUploadError(
1641
+ "TF_TILESET_LOCAL_FILE_CHANGED",
1642
+ "PMTiles archive changed during publication."
1643
+ );
1644
+ }
1645
+ function invalidRemoteResponse() {
1646
+ return new TeamTilesetUploadError(
1647
+ "TF_TILESET_UPLOAD_UNAVAILABLE",
1648
+ "Tileset upload returned an invalid response."
1649
+ );
1650
+ }
1651
+ function failureMessage(code) {
1652
+ if (code === "TF_TILESET_UPLOAD_DISABLED") return "New tileset uploads are disabled.";
1653
+ if (code === "TF_TILESET_SCHEMA_INCOMPATIBLE") {
1654
+ return "Tileset schema is incompatible with retained Map deployments.";
1655
+ }
1656
+ if (code === "TF_TILESET_UPLOAD_CHECKSUM_MISMATCH") {
1657
+ return "Tileset upload checksum validation failed.";
1658
+ }
1659
+ if (code === "TF_TILESET_UPLOAD_EXPIRED") return "Tileset upload expired.";
1660
+ return "Tileset upload failed.";
1661
+ }
1662
+ function responseError(response) {
1663
+ const body = asRecord2(response.body);
1664
+ const code = typeof body.code === "string" && /^TF_[A-Z0-9_]{1,96}$/u.test(body.code) ? body.code : "TF_TILESET_UPLOAD_UNAVAILABLE";
1665
+ const details = numericDetails(body.details);
1666
+ return new TeamTilesetUploadError(code, failureMessage(code), details);
1667
+ }
1668
+ function numericDetails(value) {
1669
+ const source = asRecord2(value);
1670
+ const details = {};
1671
+ for (const key of ["actualBytes", "limitBytes"]) {
1672
+ const candidate = source[key];
1673
+ if (Number.isSafeInteger(candidate) && candidate >= 0) {
1674
+ details[key] = candidate;
1675
+ }
1676
+ }
1677
+ return Object.freeze(details);
1678
+ }
1679
+ function parseUploadSession(value) {
1680
+ return uploadSessionSchema.parse(value);
1681
+ }
1682
+ function asRecord2(value) {
1683
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1684
+ }
1685
+ function defaultSleep(milliseconds) {
1686
+ return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
1687
+ }
1688
+ var identifierSchema = z3.string().min(20).max(80).regex(/^[A-Za-z0-9_-]+$/u);
1689
+ var uploadPartStatusSchema = z3.object({
1690
+ byteCount: z3.number().int().min(1).max(teamTilesetPartSize),
1691
+ partNumber: z3.number().int().min(1).max(239),
1692
+ uploaded: z3.boolean(),
1693
+ validationState: z3.enum(["mismatch", "pending", "processing", "valid"])
1694
+ }).strict();
1695
+ var publishedVersionSchema = z3.object({
1696
+ byteCount: z3.number().int().min(1).max(maximumTeamTilesetBytes),
1697
+ contentHash: z3.string().regex(/^[0-9a-f]{64}$/u),
1698
+ contentHashAlgorithm: z3.literal("sha256-fixed-parts-v1"),
1699
+ publishedVersion: z3.number().int().positive().max(Number.MAX_SAFE_INTEGER),
1700
+ schemaVersion: z3.literal(1),
1701
+ state: z3.literal("published"),
1702
+ versionId: identifierSchema.regex(/^tsv_[A-Za-z0-9_-]+$/u)
1703
+ }).strict();
1704
+ var uploadControlSchema = z3.object({
1705
+ byteCount: z3.number().int().min(1).max(maximumTeamTilesetBytes),
1706
+ contentHash: z3.string().regex(/^[0-9a-f]{64}$/u),
1707
+ contentHashAlgorithm: z3.literal("sha256-fixed-parts-v1"),
1708
+ expiresAt: z3.iso.datetime({ offset: true }),
1709
+ failure: z3.object({ code: z3.string().regex(/^TF_[A-Z0-9_]{1,96}$/u) }).strict().nullable(),
1710
+ partCount: z3.number().int().min(1).max(239),
1711
+ parts: z3.array(uploadPartStatusSchema).max(239),
1712
+ partSize: z3.literal(teamTilesetPartSize),
1713
+ publishedVersion: z3.null(),
1714
+ schemaVersion: z3.literal(1),
1715
+ state: z3.enum([
1716
+ "aborted",
1717
+ "aborting",
1718
+ "expired",
1719
+ "expiring",
1720
+ "initiating",
1721
+ "publishing",
1722
+ "ready_to_publish",
1723
+ "rejected",
1724
+ "rejecting",
1725
+ "uploading",
1726
+ "validating"
1727
+ ]),
1728
+ uploadId: z3.string().min(20).max(80).regex(/^tsu_[A-Za-z0-9_-]+$/u),
1729
+ versionId: z3.null()
1730
+ }).strict();
1731
+ var uploadSessionSchema = z3.union([publishedVersionSchema, uploadControlSchema]);
1732
+ var authorizationSchema = z3.object({
1733
+ expiresAt: z3.iso.datetime({ offset: true }),
1734
+ headers: z3.object({
1735
+ "Content-Length": z3.string().regex(/^[1-9][0-9]{0,9}$/u),
1736
+ "Content-MD5": z3.string().regex(/^[A-Za-z0-9+/]{22}==$/u)
1737
+ }).strict(),
1738
+ method: z3.literal("PUT"),
1739
+ partNumber: z3.number().int().min(1).max(239),
1740
+ url: z3.url().max(8192).refine((value) => {
1741
+ const url = new URL(value);
1742
+ return url.protocol === "https:" && !url.username && !url.password && !url.hash;
1743
+ })
1744
+ }).strict();
1745
+ var authorizationResponseSchema = z3.object({
1746
+ authorizations: z3.array(authorizationSchema).min(1).max(16),
1747
+ schemaVersion: z3.literal(1)
1748
+ }).strict();
1749
+ var partReceiptResponseSchema = z3.object({
1750
+ changed: z3.boolean(),
1751
+ etag: z3.string().regex(/^[0-9a-f]{32}$/u),
1752
+ partNumber: z3.number().int().min(1).max(239),
1753
+ schemaVersion: z3.literal(1)
1754
+ }).strict();
1755
+
1756
+ // src/tileset-command.ts
1757
+ function registerTilesetInspectCommand(parent, name = "inspect") {
1758
+ parent.command(name).description("Inspect one local PMTiles archive deterministically").argument("<archive>", "repository-local PMTiles archive").option("--json", "print deterministic inspection-schema-version-1 JSON").option("--no-sample", "read authoritative header and metadata without bounded MVT sampling").option(
1759
+ "--include-values <fields>",
1760
+ "include bounded observed values for comma-separated field names (requires --json)"
1761
+ ).action(
1762
+ async (archive, options) => {
1763
+ try {
1764
+ if (options.includeValues && !options.json) {
1765
+ throw inspectionOptionError("--include-values requires --json.");
1766
+ }
1767
+ const inspection = await inspectTileflowPmtiles(archive, {
1768
+ includeValues: parseIncludedValueFields(options.includeValues),
1769
+ sample: options.sample
1770
+ });
1771
+ if (options.json) {
1772
+ process.stdout.write(`${serializeCanonicalJson3(inspection)}
1773
+ `);
1774
+ return;
1775
+ }
1776
+ console.log(`${pc3.green("\u2713")} PMTiles archive is valid.`);
1777
+ console.log(` Type: ${inspection.contract.tileType}`);
1778
+ console.log(` Zooms: ${inspection.contract.minzoom}-${inspection.contract.maxzoom}`);
1779
+ console.log(` Source layers: ${inspection.contract.sourceLayers.length}`);
1780
+ if (inspection.observation) {
1781
+ console.log(
1782
+ ` Sample: ${inspection.observation.featuresRead} features in ${inspection.observation.tilesRead} tiles`
1783
+ );
1784
+ }
1785
+ for (const warning of inspection.warnings) {
1786
+ console.log(`${pc3.yellow("!")} ${warning.message}`);
1787
+ }
1788
+ } catch (error) {
1789
+ const record = asRecord3(error);
1790
+ const code = typeof record.code === "string" ? record.code : "TF_TILESET_INSPECTION_FAILED";
1791
+ const path = typeof record.path === "string" ? record.path : "archive";
1792
+ const message = error instanceof Error ? error.message : "Tileset inspection failed.";
1793
+ if (options.json) {
1794
+ process.stderr.write(
1795
+ `${serializeCanonicalJson3({ error: { code, message, path }, ok: false, schemaVersion: 1 })}
1796
+ `
1797
+ );
1798
+ } else {
1799
+ console.error(`${pc3.red("Error:")} ${message}`);
1800
+ }
1801
+ process.exitCode = 1;
1802
+ }
1803
+ }
1804
+ );
1805
+ }
1806
+ function registerTilesetCommands(program, dependencies) {
1807
+ const tileset = program.command("tileset").description("Inspect and manage geospatial tilesets");
1808
+ registerTilesetInspectCommand(tileset);
1809
+ tileset.command("publish").description("Register and publish one PMTiles archive to a Team").argument("<archive>", "repository-local PMTiles archive").requiredOption("--id <id>", "Team-local logical tileset ID").option("--name <name>", "display name").option("--attribution <text>", "data attribution").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-2 JSON").action(
1810
+ async (archive, options) => {
1811
+ let uploadFile = null;
1812
+ try {
1813
+ if (!isTilesetId(options.id)) {
1814
+ return emitFailure(options.json, "invalid_tileset_id", "Tileset ID is invalid.");
1815
+ }
1816
+ const initialIdentity = await inspectNodeTeamTilesetArchive(archive);
1817
+ await inspectTileflowPmtiles(archive, { sample: false });
1818
+ uploadFile = await openNodeTeamTilesetArchive(archive, initialIdentity);
1819
+ const prepared = await prepareTeamTilesetUpload(uploadFile);
1820
+ const authority = await resolveTeamAuthority(options, ["tilesets:write"], dependencies);
1821
+ if (!authority) return;
1822
+ const registration = await authorizedTeamRequest(authority, "/v1/tilesets", "POST", {
1823
+ body: JSON.stringify({
1824
+ ...options.attribution ? { attribution: options.attribution } : {},
1825
+ format: "pmtiles",
1826
+ name: options.name?.trim() || options.id,
1827
+ slug: options.id
1828
+ }),
1829
+ headers: {
1830
+ "Content-Type": "application/json"
1831
+ }
1832
+ });
1833
+ const registered = asRecord3(registration.body);
1834
+ const resourceId = String(registered.tilesetId ?? "");
1835
+ if (!registration.ok || !/^tls_[A-Za-z0-9_-]{16,76}$/u.test(resourceId)) {
1836
+ return emitFailure(
1837
+ options.json,
1838
+ `http_${registration.status}`,
1839
+ `Tileset registration failed (${registration.status}).`
1840
+ );
1841
+ }
1842
+ let reportedParts = 0;
1843
+ let reportedValidation = false;
1844
+ const publication = await publishPreparedTeamTilesetUpload({
1845
+ file: uploadFile,
1846
+ onProgress: options.json ? void 0 : ({ completedParts, phase, totalParts }) => {
1847
+ if (phase === "validating") {
1848
+ if (!reportedValidation) {
1849
+ process.stderr.write("Validating uploaded bytes.\n");
1850
+ reportedValidation = true;
1851
+ }
1852
+ return;
1853
+ }
1854
+ if (completedParts === totalParts || completedParts - reportedParts >= 16) {
1855
+ process.stderr.write(`Uploaded ${completedParts}/${totalParts} parts.
1856
+ `);
1857
+ reportedParts = completedParts;
1858
+ }
1859
+ },
1860
+ tilesetId: resourceId,
1861
+ transport: createHostedTeamTilesetUploadTransport({
1862
+ apiOrigin: authority.apiOrigin,
1863
+ credential: authority.credential,
1864
+ getCredential: authority.getCredential,
1865
+ tilesetId: resourceId
1866
+ }),
1867
+ upload: prepared
1868
+ });
1869
+ const document = {
1870
+ command: "tileset publish",
1871
+ publication: publication.changed ? "changed" : "unchanged",
1872
+ schemaVersion: 2,
1873
+ team: authority.team ? { id: authority.team.id, slug: authority.team.slug } : null,
1874
+ tileset: {
1875
+ byteCount: publication.byteCount,
1876
+ contentHash: publication.contentHash,
1877
+ contentHashAlgorithm: publication.contentHashAlgorithm,
1878
+ logicalId: options.id,
1879
+ state: publication.state,
1880
+ tilesetId: resourceId,
1881
+ version: { id: publication.versionId, number: publication.version }
1882
+ }
1883
+ };
1884
+ if (options.json) return emitJson(document);
1885
+ console.log(
1886
+ `${pc3.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc3.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
1887
+ );
1888
+ } catch (error) {
1889
+ if (error instanceof TeamTilesetUploadError) {
1890
+ emitFailure(options.json, error.code, error.message, error.details);
1891
+ } else {
1892
+ emitFailure(options.json, "tileset_publish_failed", "Tileset publication failed.");
1893
+ }
1894
+ } finally {
1895
+ await uploadFile?.close().catch(() => void 0);
1896
+ }
1897
+ }
1898
+ );
1899
+ tileset.command("list").description("List tilesets owned by one Team").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
1900
+ try {
1901
+ const authority = await resolveTeamAuthority(options, ["tilesets:read"], dependencies);
1902
+ if (!authority) return;
1903
+ const items = await fetchAllTeamTilesets(authority);
1904
+ const document = { command: "tileset list", schemaVersion: 1, tilesets: items };
1905
+ if (options.json) return emitJson(document);
1906
+ if (items.length === 0) return console.log(pc3.gray("No Team tilesets."));
1907
+ for (const item of items) {
1908
+ console.log(` ${pc3.bold(item.logicalName)} ${pc3.gray(item.status)}`);
1909
+ }
1910
+ } catch {
1911
+ emitFailure(options.json, "tileset_list_failed", "Tileset list failed.");
1912
+ }
1913
+ });
1914
+ tileset.command("status").description("Show one Team tileset").argument("<id>", "Team-local logical ID or opaque resource ID").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
1915
+ try {
1916
+ if (!isTilesetId(id))
1917
+ return emitFailure(options.json, "invalid_tileset_id", "Tileset ID is invalid.");
1918
+ const authority = await resolveTeamAuthority(options, ["tilesets:read"], dependencies);
1919
+ if (!authority) return;
1920
+ const response = await authorizedTeamRequest(
1921
+ authority,
1922
+ `/v1/tilesets/${encodeURIComponent(id)}`,
1923
+ "GET"
1924
+ );
1925
+ if (!response.ok) {
1926
+ return emitFailure(
1927
+ options.json,
1928
+ `http_${response.status}`,
1929
+ `Tileset status failed (${response.status}).`
1930
+ );
1931
+ }
1932
+ const parsed = tilesetDetailResponseSchema.parse(response.body);
1933
+ const document = { command: "tileset status", schemaVersion: 1, tileset: parsed.tileset };
1934
+ if (options.json) return emitJson(document);
1935
+ const item = parsed.tileset;
1936
+ console.log(`${pc3.bold(item.name)} ${pc3.gray(item.state)}`);
1937
+ console.log(` ID: ${item.id}`);
1938
+ console.log(` Current version: ${String(item.currentVersion ?? "unpublished")}`);
1939
+ } catch {
1940
+ emitFailure(options.json, "tileset_status_failed", "Tileset status failed.");
1941
+ }
1942
+ });
1943
+ tileset.command("purge").description("Permanently purge one unreferenced Team tileset").argument("<id>", "Team-local logical ID or opaque resource ID").requiredOption("--confirm <id>", "repeat the exact tileset ID").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
1944
+ try {
1945
+ if (!isTilesetId(id) || options.confirm !== id) {
1946
+ return emitFailure(
1947
+ options.json,
1948
+ "purge_confirmation_mismatch",
1949
+ "Purge confirmation must match the tileset ID."
1950
+ );
1951
+ }
1952
+ const authority = await resolveTeamAuthority(options, ["tilesets:write"], dependencies);
1953
+ if (!authority) return;
1954
+ const response = await authorizedTeamRequest(
1955
+ authority,
1956
+ `/v1/tilesets/${encodeURIComponent(id)}`,
1957
+ "DELETE",
1958
+ {
1959
+ headers: {
1960
+ "X-Tileflow-Confirm-Tileset-Purge": id
1961
+ }
1962
+ }
1963
+ );
1964
+ if (!response.ok) {
1965
+ const inUse = tilesetInUseResponseSchema.safeParse(response.body);
1966
+ if (inUse.success) {
1967
+ return emitFailure(
1968
+ options.json,
1969
+ inUse.data.code,
1970
+ "Tileset is used by retained Map deployments.",
1971
+ {
1972
+ dependencies: inUse.data.dependencies,
1973
+ nextCursor: inUse.data.nextCursor
1974
+ }
1975
+ );
1976
+ }
1977
+ return emitFailure(
1978
+ options.json,
1979
+ `http_${response.status}`,
1980
+ `Tileset purge failed (${response.status}).`
1981
+ );
1982
+ }
1983
+ const parsed = tilesetPurgeResponseSchema.parse(response.body);
1984
+ const document = { command: "tileset purge", schemaVersion: 1, ...parsed };
1985
+ if (options.json) return emitJson(document);
1986
+ console.log(`${pc3.green("\u2713")} Purge ${parsed.reclamation}.`);
1987
+ } catch {
1988
+ emitFailure(options.json, "tileset_purge_failed", "Tileset purge failed.");
1989
+ }
1990
+ });
1991
+ }
1992
+ async function fetchAllTeamTilesets(authority) {
1993
+ const items = [];
1994
+ const seenCursors = /* @__PURE__ */ new Set();
1995
+ const seenIds = /* @__PURE__ */ new Set();
1996
+ let cursor = null;
1997
+ for (let page = 0; page < 100; page += 1) {
1998
+ const query = new URLSearchParams({ limit: "100" });
1999
+ if (cursor) query.set("cursor", cursor);
2000
+ const response = await authorizedTeamRequest(
2001
+ authority,
2002
+ `/v1/tilesets?${query.toString()}`,
2003
+ "GET"
2004
+ );
2005
+ if (!response.ok) {
2006
+ throw new TeamTilesetUploadError(
2007
+ `http_${response.status}`,
2008
+ `Tileset list failed (${response.status}).`
2009
+ );
2010
+ }
2011
+ const parsed = tilesetListResponseSchema.parse(response.body);
2012
+ for (const item of parsed.tilesets) {
2013
+ if (seenIds.has(item.id)) throw new Error("Tileset list returned a duplicate resource.");
2014
+ const previous = items.at(-1);
2015
+ if (previous && (previous.logicalName > item.logicalName || previous.logicalName === item.logicalName && previous.id >= item.id)) {
2016
+ throw new Error("Tileset list returned unstable ordering.");
2017
+ }
2018
+ seenIds.add(item.id);
2019
+ items.push(item);
2020
+ }
2021
+ cursor = parsed.nextCursor ?? null;
2022
+ if (!cursor) return Object.freeze(items);
2023
+ if (seenCursors.has(cursor)) throw new Error("Tileset list repeated a cursor.");
2024
+ seenCursors.add(cursor);
2025
+ }
2026
+ throw new Error("Tileset list exceeded its safe page limit.");
2027
+ }
2028
+ function isTilesetId(value) {
2029
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value) && !["terrain", "world"].includes(value);
2030
+ }
2031
+ function parseIncludedValueFields(value) {
2032
+ if (value === void 0) return void 0;
2033
+ const fields = value.split(",").map((field) => field.trim());
2034
+ if (fields.some((field) => !field)) {
2035
+ throw inspectionOptionError("--include-values requires comma-separated field names.");
2036
+ }
2037
+ return fields;
2038
+ }
2039
+ function inspectionOptionError(message) {
2040
+ return Object.assign(new Error(message), {
2041
+ code: "TF_TILESET_INSPECTION_OPTIONS_INVALID",
2042
+ path: "includeValues"
2043
+ });
2044
+ }
2045
+ function asRecord3(value) {
2046
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2047
+ }
2048
+ var safeIdentifierSchema2 = z4.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
2049
+ var physicalTilesetIdSchema = z4.string().min(20).max(80).regex(/^tls_[A-Za-z0-9_-]+$/u);
2050
+ var cursorSchema = z4.string().min(1).max(2048).nullable();
2051
+ var tilesetInventoryItemSchema = z4.object({
2052
+ currentVersion: z4.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
2053
+ hasCurrent: z4.boolean(),
2054
+ id: physicalTilesetIdSchema,
2055
+ logicalName: z4.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
2056
+ name: z4.string().min(1).max(200),
2057
+ retainedBytes: z4.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
2058
+ retainedDeploymentCount: z4.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
2059
+ status: z4.enum(["active", "purging", "retired"]),
2060
+ updatedAt: z4.iso.datetime({ offset: true }),
2061
+ usedByMapCount: z4.number().int().min(0).max(Number.MAX_SAFE_INTEGER)
2062
+ }).strict();
2063
+ var tilesetListResponseSchema = z4.object({
2064
+ nextCursor: cursorSchema.optional(),
2065
+ schemaVersion: z4.literal(1),
2066
+ tilesets: z4.array(tilesetInventoryItemSchema).max(100)
2067
+ }).strict();
2068
+ var tilesetDependencySchema = z4.object({
2069
+ active: z4.boolean(),
2070
+ deploymentId: safeIdentifierSchema2,
2071
+ deploymentVersion: z4.number().int().positive().max(Number.MAX_SAFE_INTEGER),
2072
+ environment: z4.string().min(1).max(64),
2073
+ mapId: safeIdentifierSchema2,
2074
+ sourceId: safeIdentifierSchema2
2075
+ }).strict();
2076
+ var tilesetDetailResponseSchema = z4.object({
2077
+ schemaVersion: z4.literal(1),
2078
+ tileset: z4.object({
2079
+ attribution: z4.string().max(4096).nullable(),
2080
+ currentVersion: z4.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
2081
+ dependencies: z4.array(tilesetDependencySchema).max(25),
2082
+ dependencyNextCursor: cursorSchema,
2083
+ id: physicalTilesetIdSchema,
2084
+ name: z4.string().min(1).max(200),
2085
+ slug: z4.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
2086
+ state: z4.enum(["active", "purging", "purged"])
2087
+ }).strict()
2088
+ }).strict();
2089
+ var purgeDependencySchema = z4.object({
2090
+ deploymentId: safeIdentifierSchema2,
2091
+ mapId: safeIdentifierSchema2,
2092
+ sourceId: safeIdentifierSchema2
2093
+ }).strict();
2094
+ var tilesetInUseResponseSchema = z4.object({
2095
+ code: z4.literal("TF_TILESET_IN_USE"),
2096
+ dependencies: z4.array(purgeDependencySchema).max(32),
2097
+ error: z4.literal("Tileset is used by retained Map deployments"),
2098
+ nextCursor: cursorSchema
2099
+ }).strict();
2100
+ var tilesetPurgeResponseSchema = z4.object({
2101
+ deliveryFence: z4.enum(["complete", "pending"]),
2102
+ pendingVersions: z4.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
2103
+ reclamation: z4.enum(["complete", "pending"]),
2104
+ state: z4.enum(["purged", "purging"]),
2105
+ tilesetId: physicalTilesetIdSchema
2106
+ }).strict();
2107
+
2108
+ export {
2109
+ rejectedAccountSessionMessage,
2110
+ prepareAccountLogin,
2111
+ loadAuthConfig,
2112
+ installAccountSession,
2113
+ resolveAccountSession,
2114
+ removeAccountSession,
2115
+ writeAuthFileAtomic,
2116
+ removeAuthFile,
2117
+ normalizeApiOrigin,
2118
+ parseProjectReference,
2119
+ createCliProgram,
2120
+ hostedApiKeyOption,
2121
+ hostedApiUrlOption,
2122
+ isHostedApiKey,
2123
+ apiProfileCanTargetMap,
2124
+ requestHostedJson,
2125
+ startDeviceAuthorization,
2126
+ pollDeviceToken,
2127
+ validateAccountSession,
2128
+ validateApiKey,
2129
+ requestMapCapability,
2130
+ listMapSetupTeams,
2131
+ createManagedMapSetup,
2132
+ publishHostedStyle,
2133
+ assertHostedStyleBudget,
2134
+ uploadHostedIconPackage,
2135
+ uploadHostedFontBundle,
2136
+ fetchHostedMapStatus,
2137
+ revokeHostedAccountSession,
2138
+ resolveTeamAuthority,
2139
+ authorizedTeamRequest,
2140
+ emitJson,
2141
+ emitFailure,
2142
+ safeMessage,
2143
+ registerTilesetInspectCommand,
2144
+ registerTilesetCommands
2145
+ };