@shipstatic/types 2.5.0-beta.12 → 2.5.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -127,6 +127,7 @@ import type {
127
127
  import {
128
128
  validateApiKey,
129
129
  validateDeployToken,
130
+ validateIdempotencyKey,
130
131
  validateApiUrl,
131
132
  isDeployment,
132
133
  isBlockedExtension,
package/dist/index.d.ts CHANGED
@@ -52,11 +52,24 @@ export interface DeploymentCreateResponse extends Deployment {
52
52
  /**
53
53
  * Every path the public API answers on, declared once.
54
54
  *
55
- * The URL surface used to be written out in four places — the API's mounts,
56
- * the SDK's client, the dashboard's client, and the post-deploy smoke — so a
57
- * rename meant finding all four. Here it is one table that the producer
58
- * mounts from and the consumers request against, which is the only way a
59
- * path and its handler cannot drift apart.
55
+ * The URL surface was written out in four places — the API's mounts, the
56
+ * SDK's client, the dashboard's client, and the post-deploy smoke — so a
57
+ * rename meant finding all four. The first three now read this table.
58
+ *
59
+ * The smoke (`cloudflare/api/smoke.mjs`) deliberately still spells its own:
60
+ * five of its nine paths are `/admin/*`, which this table excludes by
61
+ * design, and splitting one list between a registry and literals reads worse
62
+ * than keeping it uniform.
63
+ *
64
+ * **What this guarantees, exactly.** Collection paths are mounted from here,
65
+ * so producer and consumer cannot diverge. Item paths are declared here and
66
+ * consumed by clients, but the API spells them relative to their mount
67
+ * (`/:deployment/config`), so the table does not *generate* them — it is
68
+ * held to them by `api/tests/architecture/api-paths.test.ts`, which fails if
69
+ * any entry names a path no route answers. Some entries have no client yet
70
+ * (`DEPLOYMENT_CONFIG`, `DOMAIN_PROPAGATION` — endpoints the SDK
71
+ * deliberately does not reach); the fence is what keeps those honest rather
72
+ * than merely asserted.
60
73
  *
61
74
  * **The operator surface is deliberately absent.** `/admin/*` paths belong
62
75
  * to `web/my`, for the same reason its row types do: this package is
@@ -293,6 +306,24 @@ export interface DomainRecordsResponse {
293
306
  /** Required DNS records for configuration */
294
307
  records: DnsRecord[];
295
308
  }
309
+ /**
310
+ * The envelope an `Idempotency-Key` must fit, and how long a replay lasts.
311
+ *
312
+ * Format lives here rather than on the server alone by the format-vs-policy
313
+ * rule: a client can decide offline whether a key is well-formed, and the
314
+ * API would reject the same value the same way.
315
+ */
316
+ export declare const IDEMPOTENCY_KEY_CONSTRAINTS: {
317
+ readonly MAX_LENGTH: 256;
318
+ /** How long a stored 201 stays replayable. */
319
+ readonly WINDOW_SECONDS: number;
320
+ };
321
+ /**
322
+ * Validate an idempotency key, returning the trimmed value or `undefined`
323
+ * when none was supplied. Throws {@link ShipError.validation} when the value
324
+ * cannot be sent — the same verdict the API would reach, reached earlier.
325
+ */
326
+ export declare function validateIdempotencyKey(value: unknown): string | undefined;
296
327
  /**
297
328
  * Response for `GET /labels` — every label in use across the caller's
298
329
  * deployments, domains and tokens, grouped and ordered by last use.
@@ -1029,6 +1060,25 @@ export interface DeploymentUploadOptions {
1029
1060
  spa?: boolean;
1030
1061
  /** @internal reCAPTCHA proof for the anonymous human deploy channel. Only available via /upload endpoint. */
1031
1062
  captcha?: string;
1063
+ /**
1064
+ * Makes this deploy replayable instead of repeatable.
1065
+ *
1066
+ * A deploy is not naturally idempotent: a client-side timeout on a slow
1067
+ * one leaves the caller unable to tell "it never landed" from "it landed
1068
+ * and the response was lost", and retrying produces a second deployment.
1069
+ * Send the same key on the retry and the platform replays the original
1070
+ * 201 verbatim rather than creating anything
1071
+ * ({@link IDEMPOTENCY_KEY_CONSTRAINTS.WINDOW_SECONDS}).
1072
+ *
1073
+ * **Agents are the audience.** A human notices a duplicate; an automated
1074
+ * retry does not. Pick a key that identifies the ATTEMPT — a run id, a
1075
+ * commit sha, a uuid minted before the first try — never one that varies
1076
+ * per attempt, which would defeat the point.
1077
+ *
1078
+ * The replay is per-caller, and it stores successes only: a failed deploy
1079
+ * retries fresh under the same key.
1080
+ */
1081
+ idempotencyKey?: string;
1032
1082
  }
1033
1083
  /**
1034
1084
  * Pagination options for every list endpoint. The response's `cursor` feeds
package/dist/index.js CHANGED
@@ -17,11 +17,24 @@ export const DeploymentStatus = {
17
17
  /**
18
18
  * Every path the public API answers on, declared once.
19
19
  *
20
- * The URL surface used to be written out in four places — the API's mounts,
21
- * the SDK's client, the dashboard's client, and the post-deploy smoke — so a
22
- * rename meant finding all four. Here it is one table that the producer
23
- * mounts from and the consumers request against, which is the only way a
24
- * path and its handler cannot drift apart.
20
+ * The URL surface was written out in four places — the API's mounts, the
21
+ * SDK's client, the dashboard's client, and the post-deploy smoke — so a
22
+ * rename meant finding all four. The first three now read this table.
23
+ *
24
+ * The smoke (`cloudflare/api/smoke.mjs`) deliberately still spells its own:
25
+ * five of its nine paths are `/admin/*`, which this table excludes by
26
+ * design, and splitting one list between a registry and literals reads worse
27
+ * than keeping it uniform.
28
+ *
29
+ * **What this guarantees, exactly.** Collection paths are mounted from here,
30
+ * so producer and consumer cannot diverge. Item paths are declared here and
31
+ * consumed by clients, but the API spells them relative to their mount
32
+ * (`/:deployment/config`), so the table does not *generate* them — it is
33
+ * held to them by `api/tests/architecture/api-paths.test.ts`, which fails if
34
+ * any entry names a path no route answers. Some entries have no client yet
35
+ * (`DEPLOYMENT_CONFIG`, `DOMAIN_PROPAGATION` — endpoints the SDK
36
+ * deliberately does not reach); the fence is what keeps those honest rather
37
+ * than merely asserted.
25
38
  *
26
39
  * **The operator surface is deliberately absent.** `/admin/*` paths belong
27
40
  * to `web/my`, for the same reason its row types do: this package is
@@ -74,6 +87,38 @@ export const DomainStatus = {
74
87
  SUCCESS: 'success',
75
88
  PAUSED: 'paused',
76
89
  };
90
+ /**
91
+ * The envelope an `Idempotency-Key` must fit, and how long a replay lasts.
92
+ *
93
+ * Format lives here rather than on the server alone by the format-vs-policy
94
+ * rule: a client can decide offline whether a key is well-formed, and the
95
+ * API would reject the same value the same way.
96
+ */
97
+ export const IDEMPOTENCY_KEY_CONSTRAINTS = {
98
+ MAX_LENGTH: 256,
99
+ /** How long a stored 201 stays replayable. */
100
+ WINDOW_SECONDS: 24 * 60 * 60,
101
+ };
102
+ /**
103
+ * Validate an idempotency key, returning the trimmed value or `undefined`
104
+ * when none was supplied. Throws {@link ShipError.validation} when the value
105
+ * cannot be sent — the same verdict the API would reach, reached earlier.
106
+ */
107
+ export function validateIdempotencyKey(value) {
108
+ if (value === undefined || value === null)
109
+ return undefined;
110
+ if (typeof value !== 'string') {
111
+ throw ShipError.validation('Idempotency key must be a string.');
112
+ }
113
+ const key = value.trim();
114
+ if (!key) {
115
+ throw ShipError.validation('Idempotency key must not be empty.');
116
+ }
117
+ if (key.length > IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH) {
118
+ throw ShipError.validation(`Idempotency key must be at most ${IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH} characters.`);
119
+ }
120
+ return key;
121
+ }
77
122
  // =============================================================================
78
123
  // ACCOUNT TYPES
79
124
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.5.0-beta.12",
3
+ "version": "2.5.0-beta.13",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -61,11 +61,24 @@ export interface DeploymentCreateResponse extends Deployment {
61
61
  /**
62
62
  * Every path the public API answers on, declared once.
63
63
  *
64
- * The URL surface used to be written out in four places — the API's mounts,
65
- * the SDK's client, the dashboard's client, and the post-deploy smoke — so a
66
- * rename meant finding all four. Here it is one table that the producer
67
- * mounts from and the consumers request against, which is the only way a
68
- * path and its handler cannot drift apart.
64
+ * The URL surface was written out in four places — the API's mounts, the
65
+ * SDK's client, the dashboard's client, and the post-deploy smoke — so a
66
+ * rename meant finding all four. The first three now read this table.
67
+ *
68
+ * The smoke (`cloudflare/api/smoke.mjs`) deliberately still spells its own:
69
+ * five of its nine paths are `/admin/*`, which this table excludes by
70
+ * design, and splitting one list between a registry and literals reads worse
71
+ * than keeping it uniform.
72
+ *
73
+ * **What this guarantees, exactly.** Collection paths are mounted from here,
74
+ * so producer and consumer cannot diverge. Item paths are declared here and
75
+ * consumed by clients, but the API spells them relative to their mount
76
+ * (`/:deployment/config`), so the table does not *generate* them — it is
77
+ * held to them by `api/tests/architecture/api-paths.test.ts`, which fails if
78
+ * any entry names a path no route answers. Some entries have no client yet
79
+ * (`DEPLOYMENT_CONFIG`, `DOMAIN_PROPAGATION` — endpoints the SDK
80
+ * deliberately does not reach); the fence is what keeps those honest rather
81
+ * than merely asserted.
69
82
  *
70
83
  * **The operator surface is deliberately absent.** `/admin/*` paths belong
71
84
  * to `web/my`, for the same reason its row types do: this package is
@@ -324,6 +337,41 @@ export interface DomainRecordsResponse {
324
337
  records: DnsRecord[];
325
338
  }
326
339
 
340
+ /**
341
+ * The envelope an `Idempotency-Key` must fit, and how long a replay lasts.
342
+ *
343
+ * Format lives here rather than on the server alone by the format-vs-policy
344
+ * rule: a client can decide offline whether a key is well-formed, and the
345
+ * API would reject the same value the same way.
346
+ */
347
+ export const IDEMPOTENCY_KEY_CONSTRAINTS = {
348
+ MAX_LENGTH: 256,
349
+ /** How long a stored 201 stays replayable. */
350
+ WINDOW_SECONDS: 24 * 60 * 60,
351
+ } as const;
352
+
353
+ /**
354
+ * Validate an idempotency key, returning the trimmed value or `undefined`
355
+ * when none was supplied. Throws {@link ShipError.validation} when the value
356
+ * cannot be sent — the same verdict the API would reach, reached earlier.
357
+ */
358
+ export function validateIdempotencyKey(value: unknown): string | undefined {
359
+ if (value === undefined || value === null) return undefined;
360
+ if (typeof value !== 'string') {
361
+ throw ShipError.validation('Idempotency key must be a string.');
362
+ }
363
+ const key = value.trim();
364
+ if (!key) {
365
+ throw ShipError.validation('Idempotency key must not be empty.');
366
+ }
367
+ if (key.length > IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH) {
368
+ throw ShipError.validation(
369
+ `Idempotency key must be at most ${IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH} characters.`,
370
+ );
371
+ }
372
+ return key;
373
+ }
374
+
327
375
  /**
328
376
  * Response for `GET /labels` — every label in use across the caller's
329
377
  * deployments, domains and tokens, grouped and ordered by last use.
@@ -1534,6 +1582,25 @@ export interface DeploymentUploadOptions {
1534
1582
  spa?: boolean;
1535
1583
  /** @internal reCAPTCHA proof for the anonymous human deploy channel. Only available via /upload endpoint. */
1536
1584
  captcha?: string;
1585
+ /**
1586
+ * Makes this deploy replayable instead of repeatable.
1587
+ *
1588
+ * A deploy is not naturally idempotent: a client-side timeout on a slow
1589
+ * one leaves the caller unable to tell "it never landed" from "it landed
1590
+ * and the response was lost", and retrying produces a second deployment.
1591
+ * Send the same key on the retry and the platform replays the original
1592
+ * 201 verbatim rather than creating anything
1593
+ * ({@link IDEMPOTENCY_KEY_CONSTRAINTS.WINDOW_SECONDS}).
1594
+ *
1595
+ * **Agents are the audience.** A human notices a duplicate; an automated
1596
+ * retry does not. Pick a key that identifies the ATTEMPT — a run id, a
1597
+ * commit sha, a uuid minted before the first try — never one that varies
1598
+ * per attempt, which would defeat the point.
1599
+ *
1600
+ * The replay is per-caller, and it stores successes only: a failed deploy
1601
+ * retries fresh under the same key.
1602
+ */
1603
+ idempotencyKey?: string;
1537
1604
  }
1538
1605
 
1539
1606
  /**