@shipstatic/ship 2.2.0-beta.5 → 2.2.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -725,6 +725,23 @@ declare const ErrorType: {
725
725
  readonly Maintenance: "maintenance";
726
726
  /** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
727
727
  readonly Network: "network_error";
728
+ /**
729
+ * A deadline expired before the exchange completed. Client-side only — set
730
+ * by HTTP clients when a timeout signal fires; never produced server-side.
731
+ *
732
+ * A member of the NETWORK category rather than a sibling of it:
733
+ * `isNetworkError()` answers "nothing was exchanged", which is true of a
734
+ * deadline exactly as it is of a refused connection, so every consumer that
735
+ * retries, declines to report, or declines to relay a wire message on that
736
+ * category is already right about a timeout. The distinct TYPE exists for
737
+ * the one decision the category cannot make — what to SAY. "Check your
738
+ * internet connection" is the wrong sentence for a five-minute deploy
739
+ * ceiling, and a surface can only tell the two apart by type.
740
+ *
741
+ * The same relationship every comparable SDK ships:
742
+ * `APIConnectionTimeoutError extends APIConnectionError`.
743
+ */
744
+ readonly Timeout: "timeout_error";
728
745
  /** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
729
746
  readonly Cancelled: "operation_cancelled";
730
747
  /** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
@@ -769,7 +786,7 @@ declare class ShipError extends Error {
769
786
  * on the client). Falls back to status-derived (401 → Authentication,
770
787
  * 403 → Forbidden, 429 → RateLimit, else → Api) for non-API responses
771
788
  * (CDN errors, intermediaries) or malformed bodies. Client-only types
772
- * (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
789
+ * (`Network`, `Timeout`, `Cancelled`, `File`, `Config`) are filtered out of the
773
790
  * trusted set — a misbehaving server claiming one of those is ignored.
774
791
  *
775
792
  * `operationName` (e.g. `"Get account"`) is used to compose the fallback
@@ -789,8 +806,9 @@ declare class ShipError extends Error {
789
806
  * Routing:
790
807
  * - Already a `ShipError` → returned as-is (caller's intent preserved)
791
808
  * - `AbortError` → `ShipError.cancelled(...)` — someone stopped it on purpose
792
- * - `TimeoutError` → `ShipError.network(...)` — a deadline expired, so
793
- * nothing was exchanged; the message names the timeout
809
+ * - `TimeoutError` → `ShipError.timeout(...)` — a deadline expired; the
810
+ * message names the timeout, and the type is in the network CATEGORY
811
+ * because nothing was exchanged
794
812
  * - A transport failure → `ShipError.network(...)` — see `isTransportFailure`
795
813
  * for what each runtime offers as evidence
796
814
  * - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server)
@@ -837,6 +855,14 @@ declare class ShipError extends Error {
837
855
  static authentication(message?: string, details?: unknown): ShipError;
838
856
  static business(message: string, status?: number, details?: unknown): ShipError;
839
857
  static network(message: string, details?: unknown): ShipError;
858
+ /**
859
+ * A deadline expired before the exchange completed.
860
+ *
861
+ * Statusless like its four client-only siblings: no exchange completed, so
862
+ * there is no HTTP status to report. `isNetworkError()` is true — see
863
+ * `ErrorType.Timeout` for why the category is shared and the type is not.
864
+ */
865
+ static timeout(message: string, details?: unknown): ShipError;
840
866
  static cancelled(message: string, details?: unknown): ShipError;
841
867
  static file(message: string, details?: unknown): ShipError;
842
868
  static config(message: string, details?: unknown): ShipError;
@@ -1964,17 +1990,45 @@ interface ShipClientOptions {
1964
1990
  deployEndpoint?: string | undefined;
1965
1991
  }
1966
1992
  /**
1967
- * Event map for Ship SDK events
1968
- * Core events for observability: request, response, error
1993
+ * Event map for Ship SDK events.
1994
+ *
1995
+ * **Every failure is visible, and the event NAME says whether it ended the
1996
+ * call.** One call emits `retry* (error | response)` — so the stream is
1997
+ * unambiguous at every prefix, and a consumer never has to wait to find out
1998
+ * what it is watching.
1999
+ *
2000
+ * `request` counts what went out; `retry` counts what failed and will be
2001
+ * tried again; `error` and `response` are the two terminal answers, exactly
2002
+ * one of which arrives.
1969
2003
  */
1970
2004
  interface ShipEvents {
1971
- /** Emitted before each API request */
2005
+ /** Emitted before each API request — once per ATTEMPT, so it counts what actually went out. */
1972
2006
  request: [url: string, init: RequestInit];
1973
- /** Emitted after successful API response */
2007
+ /** Emitted after successful API response — once, on the attempt that worked. */
1974
2008
  response: [response: Response, url: string];
1975
2009
  /**
1976
- * Emitted when something fails. TWO populations arrive here, which is why
1977
- * the type is `Error` and not `ShipError`:
2010
+ * Emitted when an attempt failed and the client is going to try again.
2011
+ * Carries the same normalized `ShipError` the terminal `error` would, plus
2012
+ * `attempt` — the number of the attempt that just failed, counting from 1,
2013
+ * which matches the arithmetic the docs use ("two retries by default, so
2014
+ * three attempts"). Under that numbering the value reads both ways at once:
2015
+ * attempt N failing IS retry N, so `retry 1 of ${maxRetries}` needs no
2016
+ * adjustment.
2017
+ *
2018
+ * This event exists so `error` can keep meaning what it always meant. When
2019
+ * retries landed, `error` fired per attempt — honest about what happened,
2020
+ * but it silently redefined the event: a consumer seeing `error, error,
2021
+ * response` could not tell "failed, retrying" from "failed, terminally" at
2022
+ * any prefix, and counting `error`s no longer counted failed calls. Two
2023
+ * names, two meanings, and nothing lost: every failure is still announced.
2024
+ *
2025
+ * A failure the loop will NOT retry is terminal and emits `error` directly,
2026
+ * never this. So is an abort that lands mid-backoff.
2027
+ */
2028
+ retry: [error: Error, url: string, attempt: number];
2029
+ /**
2030
+ * Emitted when the CALL failed — terminally, exactly once. TWO populations
2031
+ * arrive here, which is why the type is `Error` and not `ShipError`:
1978
2032
  *
1979
2033
  * - a failed request — always a `ShipError` (`executeRequest` normalizes
1980
2034
  * every failure through `ShipError.fromFetchError` before emitting), so
@@ -2052,9 +2106,16 @@ declare class ApiHttp extends SimpleEvents {
2052
2106
  * for headers, the timeout signal, the events and error normalization — so
2053
2107
  * an attempt is a whole request and nothing has to be undone between two.
2054
2108
  *
2055
- * **Events stay honest across attempts**: `request` and `error` fire per
2056
- * attempt, so a consumer counting requests sees what actually went out;
2057
- * `response` fires once, on the one that worked.
2109
+ * **Every failure is visible, and the event NAME says whether it ended the
2110
+ * call.** One call emits `retry* (error | response)`: `request` fires per
2111
+ * attempt, so a consumer counting requests sees what actually went out; a
2112
+ * failure that will be tried again is a `retry`; `error` and `response` are
2113
+ * the two terminal answers, exactly one of which arrives.
2114
+ *
2115
+ * The failure events are emitted HERE rather than in `attemptOnce`, and
2116
+ * that placement is the whole mechanism: terminality is a property of the
2117
+ * loop — of `isRetryable` and the attempt budget — so it is knowable only
2118
+ * at the one point that owns both. An attempt cannot name its own failure.
2058
2119
  *
2059
2120
  * **The caller's `timeout` governs an ATTEMPT, not the wall clock.** Each
2060
2121
  * attempt is an honest request and deserves the ceiling the caller named;
@@ -2073,7 +2134,12 @@ declare class ApiHttp extends SimpleEvents {
2073
2134
  /** Did this request carry the header that makes a repeat safe? */
2074
2135
  private hasIdempotencyKey;
2075
2136
  /**
2076
- * One attempt: headers, timeout signal, events, and error normalization.
2137
+ * One attempt: headers, timeout signal, the `request`/`response` events, and
2138
+ * error normalization.
2139
+ *
2140
+ * It does NOT emit a failure event. An attempt cannot know whether its own
2141
+ * failure ended the call — that is `executeRequest`'s question — so it
2142
+ * normalizes and throws, and the loop names what happened.
2077
2143
  */
2078
2144
  private attemptOnce;
2079
2145
  /**
@@ -2436,6 +2502,56 @@ interface MD5Result {
2436
2502
  }
2437
2503
  declare function calculateMD5(input: Blob | Buffer | string): Promise<MD5Result>;
2438
2504
 
2505
+ /**
2506
+ * @file One ordered table of deploy-file rules, and the single evaluation two
2507
+ * renderers share.
2508
+ *
2509
+ * **The defect this closes:** one rule was rendering as three sentences. A
2510
+ * file over the size cap said `File x is too large. Maximum allowed size is
2511
+ * 20MB.` from the deploy pipelines, `File size (21 MB) exceeds limit of 20 MB`
2512
+ * from `validateFiles`, and `File too large. Maximum 20971520 bytes allowed`
2513
+ * from the API — against the dual-validation doctrine that an error reads the
2514
+ * same wherever it was caught (root `CLAUDE.md`). Both pipelines also restated
2515
+ * the whole ordered check, so node/browser parity was a comment.
2516
+ *
2517
+ * **A rule states a predicate and a sentence; a renderer chooses only how to
2518
+ * DELIVER it.** That is the `SHAPES`-table move (`cli/formatters.ts`) applied
2519
+ * to validation: the throwing renderer raises the first broken rule, the
2520
+ * collecting renderer records it, and neither authors prose. Adding a rule is
2521
+ * a row, and both surfaces get it in the same position by construction.
2522
+ *
2523
+ * **Wording follows the API where a choice existed**, so the deferred Phase B
2524
+ * — promoting this table to `@shipstatic/types` with the API consuming it —
2525
+ * has less to move. Two deliberate deviations, recorded rather than silent:
2526
+ *
2527
+ * - **Sizes are formatted, not raw bytes.** The API says `20971520 bytes`;
2528
+ * a browser upload UI showing that is worse for the person reading it, and
2529
+ * the unit is the smaller half of the sentence to reconcile later.
2530
+ * - **The path is named.** The API has no path to name; the throwing renderer
2531
+ * has nothing BUT the message, so dropping it would leave a CLI user asking
2532
+ * which file.
2533
+ *
2534
+ * Out of scope, and left where they are: `validateDeployPath` (a rule about
2535
+ * the deploy PATH rather than the file, and pipelines-only), and
2536
+ * `validateFiles`' UI-tier pre-checks — empty, negative, count, unbuilt
2537
+ * marker, processing error — which have one holder each and no drift.
2538
+ */
2539
+
2540
+ /** What a rule is asked about: one file, and the deploy so far. */
2541
+ interface FileRuleInput {
2542
+ /** The path this file will be served at. */
2543
+ readonly path: string;
2544
+ /** This file's size in bytes. */
2545
+ readonly size: number;
2546
+ /** Bytes accumulated INCLUDING this file — the total rule's subject. */
2547
+ readonly totalSize: number;
2548
+ }
2549
+
2550
+ /**
2551
+ * @file Shared security validation for the deploy pipeline.
2552
+ * Used by both Node.js and browser file processing pipelines.
2553
+ */
2554
+
2439
2555
  /**
2440
2556
  * Validate a deploy path for security concerns.
2441
2557
  * Rejects paths containing path traversal patterns or null bytes.
@@ -2454,24 +2570,22 @@ declare function calculateMD5(input: Blob | Buffer | string): Promise<MD5Result>
2454
2570
  */
2455
2571
  declare function validateDeployPath(deployPath: string, sourceIdentifier: string): void;
2456
2572
  /**
2457
- * Validate a deploy file's name and extension.
2458
- * Rejects unsafe filenames (shell/URL-dangerous chars, reserved names)
2459
- * and file extensions the platform refuses to host.
2573
+ * The THROWING renderer of `FILE_RULES` — the deploy pipelines' shape.
2460
2574
  *
2461
- * **The blocklist is the platform's, delivered not this package's.** It
2462
- * arrives as `PlatformLimits.blockedExtensions` from `GET /limits`, which the
2463
- * client has already fetched by the time any file is processed. That is what
2464
- * keeps a pinned CLI from enforcing a policy the platform has moved on from,
2465
- * in either direction. Callers pass `[]` when the API sent no list (one that
2466
- * predates the field): the check then does nothing and the API refuses the
2467
- * file at the boundary, which is the correct place for it to be refused.
2575
+ * It raises the first rule the file breaks and nothing else: the rules, their
2576
+ * order and their sentences all live in `file-rules.ts`, so this function
2577
+ * cannot re-order, skip or reword one. That is what makes node/browser parity
2578
+ * structural both pipelines call this, and this calls the one table.
2468
2579
  *
2469
- * @param deployPath - The deployment path to validate
2470
- * @param sourceIdentifier - Human-readable identifier for error messages
2471
- * @param blockedExtensions - The platform's blocklist, from `/limits`
2472
- * @throws {ShipError} If the filename is unsafe or the extension is blocked
2580
+ * Its counterpart is the collecting renderer in `file-validation.ts`
2581
+ * (`validateFiles`), which reaches the same verdict and reports it as a list
2582
+ * instead of a throw.
2583
+ *
2584
+ * @param input - The file and the deploy so far (`totalSize` INCLUDES it)
2585
+ * @param limits - The platform's limits, from `/limits`
2586
+ * @throws {ShipError} The first broken rule's sentence
2473
2587
  */
2474
- declare function validateDeployFile(deployPath: string, sourceIdentifier: string, blockedExtensions: readonly string[]): void;
2588
+ declare function validateDeployFile(input: FileRuleInput, limits: PlatformLimits): void;
2475
2589
 
2476
2590
  /**
2477
2591
  * Utility functions for string manipulation.
package/dist/index.d.ts CHANGED
@@ -725,6 +725,23 @@ declare const ErrorType: {
725
725
  readonly Maintenance: "maintenance";
726
726
  /** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
727
727
  readonly Network: "network_error";
728
+ /**
729
+ * A deadline expired before the exchange completed. Client-side only — set
730
+ * by HTTP clients when a timeout signal fires; never produced server-side.
731
+ *
732
+ * A member of the NETWORK category rather than a sibling of it:
733
+ * `isNetworkError()` answers "nothing was exchanged", which is true of a
734
+ * deadline exactly as it is of a refused connection, so every consumer that
735
+ * retries, declines to report, or declines to relay a wire message on that
736
+ * category is already right about a timeout. The distinct TYPE exists for
737
+ * the one decision the category cannot make — what to SAY. "Check your
738
+ * internet connection" is the wrong sentence for a five-minute deploy
739
+ * ceiling, and a surface can only tell the two apart by type.
740
+ *
741
+ * The same relationship every comparable SDK ships:
742
+ * `APIConnectionTimeoutError extends APIConnectionError`.
743
+ */
744
+ readonly Timeout: "timeout_error";
728
745
  /** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
729
746
  readonly Cancelled: "operation_cancelled";
730
747
  /** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
@@ -769,7 +786,7 @@ declare class ShipError extends Error {
769
786
  * on the client). Falls back to status-derived (401 → Authentication,
770
787
  * 403 → Forbidden, 429 → RateLimit, else → Api) for non-API responses
771
788
  * (CDN errors, intermediaries) or malformed bodies. Client-only types
772
- * (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
789
+ * (`Network`, `Timeout`, `Cancelled`, `File`, `Config`) are filtered out of the
773
790
  * trusted set — a misbehaving server claiming one of those is ignored.
774
791
  *
775
792
  * `operationName` (e.g. `"Get account"`) is used to compose the fallback
@@ -789,8 +806,9 @@ declare class ShipError extends Error {
789
806
  * Routing:
790
807
  * - Already a `ShipError` → returned as-is (caller's intent preserved)
791
808
  * - `AbortError` → `ShipError.cancelled(...)` — someone stopped it on purpose
792
- * - `TimeoutError` → `ShipError.network(...)` — a deadline expired, so
793
- * nothing was exchanged; the message names the timeout
809
+ * - `TimeoutError` → `ShipError.timeout(...)` — a deadline expired; the
810
+ * message names the timeout, and the type is in the network CATEGORY
811
+ * because nothing was exchanged
794
812
  * - A transport failure → `ShipError.network(...)` — see `isTransportFailure`
795
813
  * for what each runtime offers as evidence
796
814
  * - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server)
@@ -837,6 +855,14 @@ declare class ShipError extends Error {
837
855
  static authentication(message?: string, details?: unknown): ShipError;
838
856
  static business(message: string, status?: number, details?: unknown): ShipError;
839
857
  static network(message: string, details?: unknown): ShipError;
858
+ /**
859
+ * A deadline expired before the exchange completed.
860
+ *
861
+ * Statusless like its four client-only siblings: no exchange completed, so
862
+ * there is no HTTP status to report. `isNetworkError()` is true — see
863
+ * `ErrorType.Timeout` for why the category is shared and the type is not.
864
+ */
865
+ static timeout(message: string, details?: unknown): ShipError;
840
866
  static cancelled(message: string, details?: unknown): ShipError;
841
867
  static file(message: string, details?: unknown): ShipError;
842
868
  static config(message: string, details?: unknown): ShipError;
@@ -1964,17 +1990,45 @@ interface ShipClientOptions {
1964
1990
  deployEndpoint?: string | undefined;
1965
1991
  }
1966
1992
  /**
1967
- * Event map for Ship SDK events
1968
- * Core events for observability: request, response, error
1993
+ * Event map for Ship SDK events.
1994
+ *
1995
+ * **Every failure is visible, and the event NAME says whether it ended the
1996
+ * call.** One call emits `retry* (error | response)` — so the stream is
1997
+ * unambiguous at every prefix, and a consumer never has to wait to find out
1998
+ * what it is watching.
1999
+ *
2000
+ * `request` counts what went out; `retry` counts what failed and will be
2001
+ * tried again; `error` and `response` are the two terminal answers, exactly
2002
+ * one of which arrives.
1969
2003
  */
1970
2004
  interface ShipEvents {
1971
- /** Emitted before each API request */
2005
+ /** Emitted before each API request — once per ATTEMPT, so it counts what actually went out. */
1972
2006
  request: [url: string, init: RequestInit];
1973
- /** Emitted after successful API response */
2007
+ /** Emitted after successful API response — once, on the attempt that worked. */
1974
2008
  response: [response: Response, url: string];
1975
2009
  /**
1976
- * Emitted when something fails. TWO populations arrive here, which is why
1977
- * the type is `Error` and not `ShipError`:
2010
+ * Emitted when an attempt failed and the client is going to try again.
2011
+ * Carries the same normalized `ShipError` the terminal `error` would, plus
2012
+ * `attempt` — the number of the attempt that just failed, counting from 1,
2013
+ * which matches the arithmetic the docs use ("two retries by default, so
2014
+ * three attempts"). Under that numbering the value reads both ways at once:
2015
+ * attempt N failing IS retry N, so `retry 1 of ${maxRetries}` needs no
2016
+ * adjustment.
2017
+ *
2018
+ * This event exists so `error` can keep meaning what it always meant. When
2019
+ * retries landed, `error` fired per attempt — honest about what happened,
2020
+ * but it silently redefined the event: a consumer seeing `error, error,
2021
+ * response` could not tell "failed, retrying" from "failed, terminally" at
2022
+ * any prefix, and counting `error`s no longer counted failed calls. Two
2023
+ * names, two meanings, and nothing lost: every failure is still announced.
2024
+ *
2025
+ * A failure the loop will NOT retry is terminal and emits `error` directly,
2026
+ * never this. So is an abort that lands mid-backoff.
2027
+ */
2028
+ retry: [error: Error, url: string, attempt: number];
2029
+ /**
2030
+ * Emitted when the CALL failed — terminally, exactly once. TWO populations
2031
+ * arrive here, which is why the type is `Error` and not `ShipError`:
1978
2032
  *
1979
2033
  * - a failed request — always a `ShipError` (`executeRequest` normalizes
1980
2034
  * every failure through `ShipError.fromFetchError` before emitting), so
@@ -2052,9 +2106,16 @@ declare class ApiHttp extends SimpleEvents {
2052
2106
  * for headers, the timeout signal, the events and error normalization — so
2053
2107
  * an attempt is a whole request and nothing has to be undone between two.
2054
2108
  *
2055
- * **Events stay honest across attempts**: `request` and `error` fire per
2056
- * attempt, so a consumer counting requests sees what actually went out;
2057
- * `response` fires once, on the one that worked.
2109
+ * **Every failure is visible, and the event NAME says whether it ended the
2110
+ * call.** One call emits `retry* (error | response)`: `request` fires per
2111
+ * attempt, so a consumer counting requests sees what actually went out; a
2112
+ * failure that will be tried again is a `retry`; `error` and `response` are
2113
+ * the two terminal answers, exactly one of which arrives.
2114
+ *
2115
+ * The failure events are emitted HERE rather than in `attemptOnce`, and
2116
+ * that placement is the whole mechanism: terminality is a property of the
2117
+ * loop — of `isRetryable` and the attempt budget — so it is knowable only
2118
+ * at the one point that owns both. An attempt cannot name its own failure.
2058
2119
  *
2059
2120
  * **The caller's `timeout` governs an ATTEMPT, not the wall clock.** Each
2060
2121
  * attempt is an honest request and deserves the ceiling the caller named;
@@ -2073,7 +2134,12 @@ declare class ApiHttp extends SimpleEvents {
2073
2134
  /** Did this request carry the header that makes a repeat safe? */
2074
2135
  private hasIdempotencyKey;
2075
2136
  /**
2076
- * One attempt: headers, timeout signal, events, and error normalization.
2137
+ * One attempt: headers, timeout signal, the `request`/`response` events, and
2138
+ * error normalization.
2139
+ *
2140
+ * It does NOT emit a failure event. An attempt cannot know whether its own
2141
+ * failure ended the call — that is `executeRequest`'s question — so it
2142
+ * normalizes and throws, and the loop names what happened.
2077
2143
  */
2078
2144
  private attemptOnce;
2079
2145
  /**
@@ -2436,6 +2502,56 @@ interface MD5Result {
2436
2502
  }
2437
2503
  declare function calculateMD5(input: Blob | Buffer | string): Promise<MD5Result>;
2438
2504
 
2505
+ /**
2506
+ * @file One ordered table of deploy-file rules, and the single evaluation two
2507
+ * renderers share.
2508
+ *
2509
+ * **The defect this closes:** one rule was rendering as three sentences. A
2510
+ * file over the size cap said `File x is too large. Maximum allowed size is
2511
+ * 20MB.` from the deploy pipelines, `File size (21 MB) exceeds limit of 20 MB`
2512
+ * from `validateFiles`, and `File too large. Maximum 20971520 bytes allowed`
2513
+ * from the API — against the dual-validation doctrine that an error reads the
2514
+ * same wherever it was caught (root `CLAUDE.md`). Both pipelines also restated
2515
+ * the whole ordered check, so node/browser parity was a comment.
2516
+ *
2517
+ * **A rule states a predicate and a sentence; a renderer chooses only how to
2518
+ * DELIVER it.** That is the `SHAPES`-table move (`cli/formatters.ts`) applied
2519
+ * to validation: the throwing renderer raises the first broken rule, the
2520
+ * collecting renderer records it, and neither authors prose. Adding a rule is
2521
+ * a row, and both surfaces get it in the same position by construction.
2522
+ *
2523
+ * **Wording follows the API where a choice existed**, so the deferred Phase B
2524
+ * — promoting this table to `@shipstatic/types` with the API consuming it —
2525
+ * has less to move. Two deliberate deviations, recorded rather than silent:
2526
+ *
2527
+ * - **Sizes are formatted, not raw bytes.** The API says `20971520 bytes`;
2528
+ * a browser upload UI showing that is worse for the person reading it, and
2529
+ * the unit is the smaller half of the sentence to reconcile later.
2530
+ * - **The path is named.** The API has no path to name; the throwing renderer
2531
+ * has nothing BUT the message, so dropping it would leave a CLI user asking
2532
+ * which file.
2533
+ *
2534
+ * Out of scope, and left where they are: `validateDeployPath` (a rule about
2535
+ * the deploy PATH rather than the file, and pipelines-only), and
2536
+ * `validateFiles`' UI-tier pre-checks — empty, negative, count, unbuilt
2537
+ * marker, processing error — which have one holder each and no drift.
2538
+ */
2539
+
2540
+ /** What a rule is asked about: one file, and the deploy so far. */
2541
+ interface FileRuleInput {
2542
+ /** The path this file will be served at. */
2543
+ readonly path: string;
2544
+ /** This file's size in bytes. */
2545
+ readonly size: number;
2546
+ /** Bytes accumulated INCLUDING this file — the total rule's subject. */
2547
+ readonly totalSize: number;
2548
+ }
2549
+
2550
+ /**
2551
+ * @file Shared security validation for the deploy pipeline.
2552
+ * Used by both Node.js and browser file processing pipelines.
2553
+ */
2554
+
2439
2555
  /**
2440
2556
  * Validate a deploy path for security concerns.
2441
2557
  * Rejects paths containing path traversal patterns or null bytes.
@@ -2454,24 +2570,22 @@ declare function calculateMD5(input: Blob | Buffer | string): Promise<MD5Result>
2454
2570
  */
2455
2571
  declare function validateDeployPath(deployPath: string, sourceIdentifier: string): void;
2456
2572
  /**
2457
- * Validate a deploy file's name and extension.
2458
- * Rejects unsafe filenames (shell/URL-dangerous chars, reserved names)
2459
- * and file extensions the platform refuses to host.
2573
+ * The THROWING renderer of `FILE_RULES` — the deploy pipelines' shape.
2460
2574
  *
2461
- * **The blocklist is the platform's, delivered not this package's.** It
2462
- * arrives as `PlatformLimits.blockedExtensions` from `GET /limits`, which the
2463
- * client has already fetched by the time any file is processed. That is what
2464
- * keeps a pinned CLI from enforcing a policy the platform has moved on from,
2465
- * in either direction. Callers pass `[]` when the API sent no list (one that
2466
- * predates the field): the check then does nothing and the API refuses the
2467
- * file at the boundary, which is the correct place for it to be refused.
2575
+ * It raises the first rule the file breaks and nothing else: the rules, their
2576
+ * order and their sentences all live in `file-rules.ts`, so this function
2577
+ * cannot re-order, skip or reword one. That is what makes node/browser parity
2578
+ * structural both pipelines call this, and this calls the one table.
2468
2579
  *
2469
- * @param deployPath - The deployment path to validate
2470
- * @param sourceIdentifier - Human-readable identifier for error messages
2471
- * @param blockedExtensions - The platform's blocklist, from `/limits`
2472
- * @throws {ShipError} If the filename is unsafe or the extension is blocked
2580
+ * Its counterpart is the collecting renderer in `file-validation.ts`
2581
+ * (`validateFiles`), which reaches the same verdict and reports it as a list
2582
+ * instead of a throw.
2583
+ *
2584
+ * @param input - The file and the deploy so far (`totalSize` INCLUDES it)
2585
+ * @param limits - The platform's limits, from `/limits`
2586
+ * @throws {ShipError} The first broken rule's sentence
2473
2587
  */
2474
- declare function validateDeployFile(deployPath: string, sourceIdentifier: string, blockedExtensions: readonly string[]): void;
2588
+ declare function validateDeployFile(input: FileRuleInput, limits: PlatformLimits): void;
2475
2589
 
2476
2590
  /**
2477
2591
  * Utility functions for string manipulation.