@shipstatic/types 2.7.0-beta.4 → 2.8.0-beta.1

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.ts CHANGED
@@ -1118,6 +1118,39 @@ export declare const DEPLOY_TOKEN: {
1118
1118
  /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 39`). */
1119
1119
  readonly TOTAL_LENGTH: 39;
1120
1120
  };
1121
+ /**
1122
+ * Shape constants for OAuth access tokens (`oauth-{32 hex chars}`) — the
1123
+ * delegated population, minted by the platform's own authorization server
1124
+ * for a connected app acting on a user's behalf.
1125
+ *
1126
+ * Same width as the other two, and for the same reason: one entropy standard
1127
+ * across the platform, so "how long is a credential" has one answer.
1128
+ *
1129
+ * **This population is the access token alone.** Refresh tokens, authorization
1130
+ * codes and client secrets are deliberately NOT here and are deliberately not
1131
+ * prefixed by this constant: none of them ever enters the `Authorization:
1132
+ * Bearer` slot — a refresh token is posted as a form field to the token
1133
+ * endpoint, which knows what it is receiving — so `classifyToken` never sees
1134
+ * one and a prefix would name a population no dispatcher dispatches. The same
1135
+ * reasoning that keeps the deployment claim code bare.
1136
+ *
1137
+ * **The prefix must be applied at the MINT, never as a display wrapper.** The
1138
+ * authorization server hashes what it stores and the API hashes what it is
1139
+ * presented, so the prefix has to be inside the hashed string on both sides.
1140
+ * `@better-auth/oauth-provider` offers a `prefix.opaqueAccessToken` option
1141
+ * that prepends AFTER hashing and strips on its own read paths; using it would
1142
+ * store a hash of the UNPREFIXED token and silently break the platform's read
1143
+ * arm. The API therefore mints through `generateOpaqueAccessToken` — recorded
1144
+ * beside the config in `cloudflare/api/src/lib/auth/instance.ts`.
1145
+ */
1146
+ export declare const OAUTH_TOKEN: {
1147
+ /** Prefix that identifies an OAuth access token. */
1148
+ readonly PREFIX: "oauth-";
1149
+ /** Number of hex characters following the prefix. */
1150
+ readonly HEX_LENGTH: 32;
1151
+ /** Total length including prefix (`PREFIX.length + HEX_LENGTH = 38`). */
1152
+ readonly TOTAL_LENGTH: 38;
1153
+ };
1121
1154
  /**
1122
1155
  * Shape constants for caller identifiers (the `X-Caller` instance-identity
1123
1156
  * header — rate-limit bucketing for multi-tenant orchestrators). The API
@@ -1138,16 +1171,23 @@ export declare const CALLER: {
1138
1171
  * client token in one wire slot (`Authorization: Bearer <value>`) and
1139
1172
  * classifies by value, never by a side channel — this is the classifier.
1140
1173
  *
1141
- * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
1142
- * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
1143
- * straight into an auth method and the pair can never drift. `OPAQUE` is any
1144
- * other value — shape says nothing about it, so only a lookup can. Today the
1145
- * server refuses every opaque bearer; the OAuth access-token population
1146
- * resolves there when the authorization server ships.
1174
+ * `API_KEY`, `DEPLOY_TOKEN` and `OAUTH` *are* `AuthMethod.API_KEY`,
1175
+ * `AuthMethod.TOKEN` and `AuthMethod.OAUTH` — the equality is structural, so
1176
+ * a classification flows straight into an auth method and the trio can never
1177
+ * drift.
1178
+ *
1179
+ * `OPAQUE` is any other value, and since 2026-08-14 it names NO population:
1180
+ * every credential this platform mints for the Bearer slot carries a prefix,
1181
+ * so an opaque bearer is a bearer we did not mint. It stays a member rather
1182
+ * than becoming a `null` return because a dispatcher with a total codomain
1183
+ * reads better than one with an absence in it — and because it is where a
1184
+ * future population would land before anyone gave it a shape, which is
1185
+ * exactly what the OAuth token itself did until its prefix existed.
1147
1186
  */
1148
1187
  export declare const TokenKind: {
1149
1188
  readonly API_KEY: "apiKey";
1150
1189
  readonly DEPLOY_TOKEN: "token";
1190
+ readonly OAUTH: "oauth";
1151
1191
  readonly OPAQUE: "opaque";
1152
1192
  };
1153
1193
  export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
@@ -1243,11 +1283,22 @@ export declare function validateApiKey(apiKey: string): void;
1243
1283
  * Validate deploy token format
1244
1284
  */
1245
1285
  export declare function validateDeployToken(deployToken: string): void;
1286
+ /**
1287
+ * Validate OAuth access token format
1288
+ */
1289
+ export declare function validateOAuthToken(oauthToken: string): void;
1246
1290
  /**
1247
1291
  * Validate a client token of any population. Classifies by shape and applies
1248
- * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
1249
- * validated strictly; opaque tokens (OAuth access tokens, future populations)
1250
- * only need to be non-empty — their validity is the server's to decide.
1292
+ * the matching format rules: all three prefixed populations are validated
1293
+ * strictly; an OPAQUE token only needs to be non-empty.
1294
+ *
1295
+ * **The OPAQUE arm stays permissive on purpose**, even though the platform no
1296
+ * longer mints an unprefixed credential. It is the fallback for a population
1297
+ * that does not exist yet, and a client refusing a shape the server would
1298
+ * accept is the one failure mode this boundary must never have — the server
1299
+ * decides, and it refuses an unrecognised bearer anyway. Unprefixed OAuth
1300
+ * tokens from before 2026-08-14 land here and are refused server-side, which
1301
+ * is correct: they were revoked by the change, not grandfathered.
1251
1302
  */
1252
1303
  export declare function validateToken(token: string): void;
1253
1304
  /**
package/dist/index.js CHANGED
@@ -1020,7 +1020,7 @@ export function hasUnbuiltMarker(filePath) {
1020
1020
  // delegated-access scopes (OAuthScope).
1021
1021
  //
1022
1022
  // THE SHAPE LAW, in three clauses, over the `Authorization: Bearer` slot's
1023
- // two populations below. The deployment claim code is the API's own
1023
+ // three populations below. The deployment claim code is the API's own
1024
1024
  // (`AUTH.CLAIM`, server-side: the API mints it and the API validates it, so
1025
1025
  // it has one holder and stays there) and shares only clause 1 — it is the
1026
1026
  // platform's one deliberately BARE secret, because it never enters the
@@ -1037,9 +1037,12 @@ export function hasUnbuiltMarker(filePath) {
1037
1037
  //
1038
1038
  // 2. EVERY BEARER POPULATION IS NAMED BY ITS PREFIX. A credential says what
1039
1039
  // it is before anything parses it — which is what lets `classifyToken`
1040
- // below dispatch two populations sharing one `Authorization: Bearer`
1040
+ // below dispatch three populations sharing one `Authorization: Bearer`
1041
1041
  // slot, and what lets a value found in a log, a support ticket or a
1042
- // pasted URL be recognised and revoked on sight.
1042
+ // pasted URL be recognised and revoked on sight. The OAuth access token
1043
+ // was this clause's one standing exception until 2026-08-14 — the
1044
+ // authorization server it was born on had no mint hook to give it a
1045
+ // prefix, and its successor does.
1043
1046
  //
1044
1047
  // 3. NO PREFIX IS A PREFIX OF ANOTHER. This is what makes the dispatch
1045
1048
  // order-independent, and it is the reason the populations are named on
@@ -1104,6 +1107,39 @@ export const DEPLOY_TOKEN = {
1104
1107
  /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 39`). */
1105
1108
  TOTAL_LENGTH: 39,
1106
1109
  };
1110
+ /**
1111
+ * Shape constants for OAuth access tokens (`oauth-{32 hex chars}`) — the
1112
+ * delegated population, minted by the platform's own authorization server
1113
+ * for a connected app acting on a user's behalf.
1114
+ *
1115
+ * Same width as the other two, and for the same reason: one entropy standard
1116
+ * across the platform, so "how long is a credential" has one answer.
1117
+ *
1118
+ * **This population is the access token alone.** Refresh tokens, authorization
1119
+ * codes and client secrets are deliberately NOT here and are deliberately not
1120
+ * prefixed by this constant: none of them ever enters the `Authorization:
1121
+ * Bearer` slot — a refresh token is posted as a form field to the token
1122
+ * endpoint, which knows what it is receiving — so `classifyToken` never sees
1123
+ * one and a prefix would name a population no dispatcher dispatches. The same
1124
+ * reasoning that keeps the deployment claim code bare.
1125
+ *
1126
+ * **The prefix must be applied at the MINT, never as a display wrapper.** The
1127
+ * authorization server hashes what it stores and the API hashes what it is
1128
+ * presented, so the prefix has to be inside the hashed string on both sides.
1129
+ * `@better-auth/oauth-provider` offers a `prefix.opaqueAccessToken` option
1130
+ * that prepends AFTER hashing and strips on its own read paths; using it would
1131
+ * store a hash of the UNPREFIXED token and silently break the platform's read
1132
+ * arm. The API therefore mints through `generateOpaqueAccessToken` — recorded
1133
+ * beside the config in `cloudflare/api/src/lib/auth/instance.ts`.
1134
+ */
1135
+ export const OAUTH_TOKEN = {
1136
+ /** Prefix that identifies an OAuth access token. */
1137
+ PREFIX: 'oauth-',
1138
+ /** Number of hex characters following the prefix. */
1139
+ HEX_LENGTH: 32,
1140
+ /** Total length including prefix (`PREFIX.length + HEX_LENGTH = 38`). */
1141
+ TOTAL_LENGTH: 38,
1142
+ };
1107
1143
  /**
1108
1144
  * Shape constants for caller identifiers (the `X-Caller` instance-identity
1109
1145
  * header — rate-limit bucketing for multi-tenant orchestrators). The API
@@ -1124,16 +1160,23 @@ export const CALLER = {
1124
1160
  * client token in one wire slot (`Authorization: Bearer <value>`) and
1125
1161
  * classifies by value, never by a side channel — this is the classifier.
1126
1162
  *
1127
- * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
1128
- * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
1129
- * straight into an auth method and the pair can never drift. `OPAQUE` is any
1130
- * other value — shape says nothing about it, so only a lookup can. Today the
1131
- * server refuses every opaque bearer; the OAuth access-token population
1132
- * resolves there when the authorization server ships.
1163
+ * `API_KEY`, `DEPLOY_TOKEN` and `OAUTH` *are* `AuthMethod.API_KEY`,
1164
+ * `AuthMethod.TOKEN` and `AuthMethod.OAUTH` — the equality is structural, so
1165
+ * a classification flows straight into an auth method and the trio can never
1166
+ * drift.
1167
+ *
1168
+ * `OPAQUE` is any other value, and since 2026-08-14 it names NO population:
1169
+ * every credential this platform mints for the Bearer slot carries a prefix,
1170
+ * so an opaque bearer is a bearer we did not mint. It stays a member rather
1171
+ * than becoming a `null` return because a dispatcher with a total codomain
1172
+ * reads better than one with an absence in it — and because it is where a
1173
+ * future population would land before anyone gave it a shape, which is
1174
+ * exactly what the OAuth token itself did until its prefix existed.
1133
1175
  */
1134
1176
  export const TokenKind = {
1135
1177
  API_KEY: AuthMethod.API_KEY,
1136
1178
  DEPLOY_TOKEN: AuthMethod.TOKEN,
1179
+ OAUTH: AuthMethod.OAUTH,
1137
1180
  OPAQUE: 'opaque',
1138
1181
  };
1139
1182
  /**
@@ -1147,6 +1190,8 @@ export function classifyToken(token) {
1147
1190
  return TokenKind.API_KEY;
1148
1191
  if (token.startsWith(DEPLOY_TOKEN.PREFIX))
1149
1192
  return TokenKind.DEPLOY_TOKEN;
1193
+ if (token.startsWith(OAUTH_TOKEN.PREFIX))
1194
+ return TokenKind.OAUTH;
1150
1195
  return TokenKind.OPAQUE;
1151
1196
  }
1152
1197
  /**
@@ -1273,11 +1318,24 @@ export function validateApiKey(apiKey) {
1273
1318
  export function validateDeployToken(deployToken) {
1274
1319
  validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
1275
1320
  }
1321
+ /**
1322
+ * Validate OAuth access token format
1323
+ */
1324
+ export function validateOAuthToken(oauthToken) {
1325
+ validatePrefixedCredential(oauthToken, OAUTH_TOKEN, 'OAuth access token');
1326
+ }
1276
1327
  /**
1277
1328
  * Validate a client token of any population. Classifies by shape and applies
1278
- * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
1279
- * validated strictly; opaque tokens (OAuth access tokens, future populations)
1280
- * only need to be non-empty — their validity is the server's to decide.
1329
+ * the matching format rules: all three prefixed populations are validated
1330
+ * strictly; an OPAQUE token only needs to be non-empty.
1331
+ *
1332
+ * **The OPAQUE arm stays permissive on purpose**, even though the platform no
1333
+ * longer mints an unprefixed credential. It is the fallback for a population
1334
+ * that does not exist yet, and a client refusing a shape the server would
1335
+ * accept is the one failure mode this boundary must never have — the server
1336
+ * decides, and it refuses an unrecognised bearer anyway. Unprefixed OAuth
1337
+ * tokens from before 2026-08-14 land here and are refused server-side, which
1338
+ * is correct: they were revoked by the change, not grandfathered.
1281
1339
  */
1282
1340
  export function validateToken(token) {
1283
1341
  switch (classifyToken(token)) {
@@ -1287,6 +1345,9 @@ export function validateToken(token) {
1287
1345
  case TokenKind.DEPLOY_TOKEN:
1288
1346
  validateDeployToken(token);
1289
1347
  return;
1348
+ case TokenKind.OAUTH:
1349
+ validateOAuthToken(token);
1350
+ return;
1290
1351
  case TokenKind.OPAQUE:
1291
1352
  if (!token)
1292
1353
  throw ShipError.validation('Token must be a non-empty string');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.7.0-beta.4",
3
+ "version": "2.8.0-beta.1",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "url": "https://github.com/shipstatic/types/issues"
42
42
  },
43
43
  "engines": {
44
- "node": ">=20.0.0"
44
+ "node": ">=20.19.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@biomejs/biome": "2.5.5",
package/src/index.ts CHANGED
@@ -1702,7 +1702,7 @@ export interface PingResponse {
1702
1702
  // delegated-access scopes (OAuthScope).
1703
1703
  //
1704
1704
  // THE SHAPE LAW, in three clauses, over the `Authorization: Bearer` slot's
1705
- // two populations below. The deployment claim code is the API's own
1705
+ // three populations below. The deployment claim code is the API's own
1706
1706
  // (`AUTH.CLAIM`, server-side: the API mints it and the API validates it, so
1707
1707
  // it has one holder and stays there) and shares only clause 1 — it is the
1708
1708
  // platform's one deliberately BARE secret, because it never enters the
@@ -1719,9 +1719,12 @@ export interface PingResponse {
1719
1719
  //
1720
1720
  // 2. EVERY BEARER POPULATION IS NAMED BY ITS PREFIX. A credential says what
1721
1721
  // it is before anything parses it — which is what lets `classifyToken`
1722
- // below dispatch two populations sharing one `Authorization: Bearer`
1722
+ // below dispatch three populations sharing one `Authorization: Bearer`
1723
1723
  // slot, and what lets a value found in a log, a support ticket or a
1724
- // pasted URL be recognised and revoked on sight.
1724
+ // pasted URL be recognised and revoked on sight. The OAuth access token
1725
+ // was this clause's one standing exception until 2026-08-14 — the
1726
+ // authorization server it was born on had no mint hook to give it a
1727
+ // prefix, and its successor does.
1725
1728
  //
1726
1729
  // 3. NO PREFIX IS A PREFIX OF ANOTHER. This is what makes the dispatch
1727
1730
  // order-independent, and it is the reason the populations are named on
@@ -1793,6 +1796,40 @@ export const DEPLOY_TOKEN = {
1793
1796
  TOTAL_LENGTH: 39,
1794
1797
  } as const;
1795
1798
 
1799
+ /**
1800
+ * Shape constants for OAuth access tokens (`oauth-{32 hex chars}`) — the
1801
+ * delegated population, minted by the platform's own authorization server
1802
+ * for a connected app acting on a user's behalf.
1803
+ *
1804
+ * Same width as the other two, and for the same reason: one entropy standard
1805
+ * across the platform, so "how long is a credential" has one answer.
1806
+ *
1807
+ * **This population is the access token alone.** Refresh tokens, authorization
1808
+ * codes and client secrets are deliberately NOT here and are deliberately not
1809
+ * prefixed by this constant: none of them ever enters the `Authorization:
1810
+ * Bearer` slot — a refresh token is posted as a form field to the token
1811
+ * endpoint, which knows what it is receiving — so `classifyToken` never sees
1812
+ * one and a prefix would name a population no dispatcher dispatches. The same
1813
+ * reasoning that keeps the deployment claim code bare.
1814
+ *
1815
+ * **The prefix must be applied at the MINT, never as a display wrapper.** The
1816
+ * authorization server hashes what it stores and the API hashes what it is
1817
+ * presented, so the prefix has to be inside the hashed string on both sides.
1818
+ * `@better-auth/oauth-provider` offers a `prefix.opaqueAccessToken` option
1819
+ * that prepends AFTER hashing and strips on its own read paths; using it would
1820
+ * store a hash of the UNPREFIXED token and silently break the platform's read
1821
+ * arm. The API therefore mints through `generateOpaqueAccessToken` — recorded
1822
+ * beside the config in `cloudflare/api/src/lib/auth/instance.ts`.
1823
+ */
1824
+ export const OAUTH_TOKEN = {
1825
+ /** Prefix that identifies an OAuth access token. */
1826
+ PREFIX: 'oauth-',
1827
+ /** Number of hex characters following the prefix. */
1828
+ HEX_LENGTH: 32,
1829
+ /** Total length including prefix (`PREFIX.length + HEX_LENGTH = 38`). */
1830
+ TOTAL_LENGTH: 38,
1831
+ } as const;
1832
+
1796
1833
  /**
1797
1834
  * Shape constants for caller identifiers (the `X-Caller` instance-identity
1798
1835
  * header — rate-limit bucketing for multi-tenant orchestrators). The API
@@ -1814,16 +1851,23 @@ export const CALLER = {
1814
1851
  * client token in one wire slot (`Authorization: Bearer <value>`) and
1815
1852
  * classifies by value, never by a side channel — this is the classifier.
1816
1853
  *
1817
- * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
1818
- * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
1819
- * straight into an auth method and the pair can never drift. `OPAQUE` is any
1820
- * other value — shape says nothing about it, so only a lookup can. Today the
1821
- * server refuses every opaque bearer; the OAuth access-token population
1822
- * resolves there when the authorization server ships.
1854
+ * `API_KEY`, `DEPLOY_TOKEN` and `OAUTH` *are* `AuthMethod.API_KEY`,
1855
+ * `AuthMethod.TOKEN` and `AuthMethod.OAUTH` — the equality is structural, so
1856
+ * a classification flows straight into an auth method and the trio can never
1857
+ * drift.
1858
+ *
1859
+ * `OPAQUE` is any other value, and since 2026-08-14 it names NO population:
1860
+ * every credential this platform mints for the Bearer slot carries a prefix,
1861
+ * so an opaque bearer is a bearer we did not mint. It stays a member rather
1862
+ * than becoming a `null` return because a dispatcher with a total codomain
1863
+ * reads better than one with an absence in it — and because it is where a
1864
+ * future population would land before anyone gave it a shape, which is
1865
+ * exactly what the OAuth token itself did until its prefix existed.
1823
1866
  */
1824
1867
  export const TokenKind = {
1825
1868
  API_KEY: AuthMethod.API_KEY,
1826
1869
  DEPLOY_TOKEN: AuthMethod.TOKEN,
1870
+ OAUTH: AuthMethod.OAUTH,
1827
1871
  OPAQUE: 'opaque',
1828
1872
  } as const;
1829
1873
 
@@ -1838,6 +1882,7 @@ export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
1838
1882
  export function classifyToken(token: string): TokenKindType {
1839
1883
  if (token.startsWith(API_KEY.PREFIX)) return TokenKind.API_KEY;
1840
1884
  if (token.startsWith(DEPLOY_TOKEN.PREFIX)) return TokenKind.DEPLOY_TOKEN;
1885
+ if (token.startsWith(OAUTH_TOKEN.PREFIX)) return TokenKind.OAUTH;
1841
1886
  return TokenKind.OPAQUE;
1842
1887
  }
1843
1888
 
@@ -1988,11 +2033,25 @@ export function validateDeployToken(deployToken: string): void {
1988
2033
  validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
1989
2034
  }
1990
2035
 
2036
+ /**
2037
+ * Validate OAuth access token format
2038
+ */
2039
+ export function validateOAuthToken(oauthToken: string): void {
2040
+ validatePrefixedCredential(oauthToken, OAUTH_TOKEN, 'OAuth access token');
2041
+ }
2042
+
1991
2043
  /**
1992
2044
  * Validate a client token of any population. Classifies by shape and applies
1993
- * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
1994
- * validated strictly; opaque tokens (OAuth access tokens, future populations)
1995
- * only need to be non-empty — their validity is the server's to decide.
2045
+ * the matching format rules: all three prefixed populations are validated
2046
+ * strictly; an OPAQUE token only needs to be non-empty.
2047
+ *
2048
+ * **The OPAQUE arm stays permissive on purpose**, even though the platform no
2049
+ * longer mints an unprefixed credential. It is the fallback for a population
2050
+ * that does not exist yet, and a client refusing a shape the server would
2051
+ * accept is the one failure mode this boundary must never have — the server
2052
+ * decides, and it refuses an unrecognised bearer anyway. Unprefixed OAuth
2053
+ * tokens from before 2026-08-14 land here and are refused server-side, which
2054
+ * is correct: they were revoked by the change, not grandfathered.
1996
2055
  */
1997
2056
  export function validateToken(token: string): void {
1998
2057
  switch (classifyToken(token)) {
@@ -2002,6 +2061,9 @@ export function validateToken(token: string): void {
2002
2061
  case TokenKind.DEPLOY_TOKEN:
2003
2062
  validateDeployToken(token);
2004
2063
  return;
2064
+ case TokenKind.OAUTH:
2065
+ validateOAuthToken(token);
2066
+ return;
2005
2067
  case TokenKind.OPAQUE:
2006
2068
  if (!token) throw ShipError.validation('Token must be a non-empty string');
2007
2069
  }