@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 +60 -9
- package/dist/index.js +73 -12
- package/package.json +2 -2
- package/src/index.ts +74 -12
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 `
|
|
1142
|
-
* `AuthMethod.TOKEN` — the equality is structural, so
|
|
1143
|
-
* straight into an auth method and the
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
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:
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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 `
|
|
1128
|
-
* `AuthMethod.TOKEN` — the equality is structural, so
|
|
1129
|
-
* straight into an auth method and the
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
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:
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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 `
|
|
1818
|
-
* `AuthMethod.TOKEN` — the equality is structural, so
|
|
1819
|
-
* straight into an auth method and the
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
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:
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
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
|
}
|