@shipstatic/types 2.7.0 → 2.8.0-beta.2
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 +97 -9
- package/dist/index.js +120 -15
- package/package.json +1 -1
- package/src/index.ts +122 -15
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];
|
|
@@ -1158,6 +1198,43 @@ export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
|
|
|
1158
1198
|
* is what guarantees client and server can never disagree on dispatch.
|
|
1159
1199
|
*/
|
|
1160
1200
|
export declare function classifyToken(token: string): TokenKindType;
|
|
1201
|
+
/**
|
|
1202
|
+
* Read the credential out of an `Authorization` header value — the step
|
|
1203
|
+
* BEFORE `classifyToken`, and the other half of the one wire slot this
|
|
1204
|
+
* section owns.
|
|
1205
|
+
*
|
|
1206
|
+
* Returns the credential's own bytes, or `null` when the header carries a
|
|
1207
|
+
* foreign scheme or nothing after the scheme.
|
|
1208
|
+
*
|
|
1209
|
+
* **The scheme is folded; the credential is not.** RFC 7235 §2.1 makes the
|
|
1210
|
+
* auth-scheme case-insensitive, so `bearer`, `Bearer` and `BEARER` are the
|
|
1211
|
+
* same header. The value after it is opaque and is compared literally
|
|
1212
|
+
* everywhere it is used — `ship-`/`deploy-`/`oauth-` are lowercase hex, and
|
|
1213
|
+
* folding them would make a credential match values it is not.
|
|
1214
|
+
*
|
|
1215
|
+
* **This platform has paid for the rule twice, which is why it has an owner
|
|
1216
|
+
* rather than a convention.** A spec-conformant `bearer ship-…` client was
|
|
1217
|
+
* refused for as long as the API's scheme test was spelled case-sensitively;
|
|
1218
|
+
* and `@better-auth/oauth-provider` carries the same defect in four places
|
|
1219
|
+
* today (`startsWith("Bearer ")`), which is precisely why the platform folds
|
|
1220
|
+
* the scheme itself and hands the provider a bare token.
|
|
1221
|
+
*
|
|
1222
|
+
* **ABSENCE is deliberately not this function's business.** A missing header
|
|
1223
|
+
* and an unreadable one are different facts, and the callers that care split
|
|
1224
|
+
* on them: the API worker's middleware distinguishes `absent` (the only
|
|
1225
|
+
* anonymous path) from `unreadable` (a presented credential that is refused),
|
|
1226
|
+
* and collapsing the two here would take that distinction away from the layer
|
|
1227
|
+
* that needs it. Callers check for the header themselves and pass its value.
|
|
1228
|
+
*
|
|
1229
|
+
* **Why this lives in the constitution rather than in a worker's `shared/`.**
|
|
1230
|
+
* It is the same wire boundary `classifyToken` already owns — one reads the
|
|
1231
|
+
* slot, the other dispatches on what came out — and a rule with two holders
|
|
1232
|
+
* whose drift is silent earns exactly one owner regardless of what the
|
|
1233
|
+
* convoy costs. The estate's recorded refusal to own a `Bearer` CONSTANT
|
|
1234
|
+
* stands and is a different thing: that is RFC vocabulary, the same reason
|
|
1235
|
+
* this package owns no `"POST"`. A parser is not a spelling.
|
|
1236
|
+
*/
|
|
1237
|
+
export declare function readBearerValue(header: string): string | null;
|
|
1161
1238
|
/**
|
|
1162
1239
|
* OAuth scope vocabulary for delegated third-party access tokens.
|
|
1163
1240
|
* Single source of truth used by the authorization server (advertised in
|
|
@@ -1243,11 +1320,22 @@ export declare function validateApiKey(apiKey: string): void;
|
|
|
1243
1320
|
* Validate deploy token format
|
|
1244
1321
|
*/
|
|
1245
1322
|
export declare function validateDeployToken(deployToken: string): void;
|
|
1323
|
+
/**
|
|
1324
|
+
* Validate OAuth access token format
|
|
1325
|
+
*/
|
|
1326
|
+
export declare function validateOAuthToken(oauthToken: string): void;
|
|
1246
1327
|
/**
|
|
1247
1328
|
* Validate a client token of any population. Classifies by shape and applies
|
|
1248
|
-
* the matching format rules:
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
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.
|
|
1251
1339
|
*/
|
|
1252
1340
|
export declare function validateToken(token: string): void;
|
|
1253
1341
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1015,12 +1015,13 @@ export function hasUnbuiltMarker(filePath) {
|
|
|
1015
1015
|
// =============================================================================
|
|
1016
1016
|
// The one address for credential vocabulary: where human identity lives
|
|
1017
1017
|
// (AUTH_BASE_PATH), how a request is authorized (AuthMethod), the shapes
|
|
1018
|
-
// that distinguish populations on the wire (API_KEY, DEPLOY_TOKEN,
|
|
1019
|
-
// the
|
|
1020
|
-
//
|
|
1018
|
+
// that distinguish populations on the wire (API_KEY, DEPLOY_TOKEN,
|
|
1019
|
+
// OAUTH_TOKEN, CALLER), the two halves of the one Bearer slot
|
|
1020
|
+
// (readBearerValue reads it, classifyToken/TokenKind dispatch on what came
|
|
1021
|
+
// out), and the delegated-access scopes (OAuthScope).
|
|
1021
1022
|
//
|
|
1022
1023
|
// THE SHAPE LAW, in three clauses, over the `Authorization: Bearer` slot's
|
|
1023
|
-
//
|
|
1024
|
+
// three populations below. The deployment claim code is the API's own
|
|
1024
1025
|
// (`AUTH.CLAIM`, server-side: the API mints it and the API validates it, so
|
|
1025
1026
|
// it has one holder and stays there) and shares only clause 1 — it is the
|
|
1026
1027
|
// platform's one deliberately BARE secret, because it never enters the
|
|
@@ -1037,9 +1038,12 @@ export function hasUnbuiltMarker(filePath) {
|
|
|
1037
1038
|
//
|
|
1038
1039
|
// 2. EVERY BEARER POPULATION IS NAMED BY ITS PREFIX. A credential says what
|
|
1039
1040
|
// it is before anything parses it — which is what lets `classifyToken`
|
|
1040
|
-
// below dispatch
|
|
1041
|
+
// below dispatch three populations sharing one `Authorization: Bearer`
|
|
1041
1042
|
// slot, and what lets a value found in a log, a support ticket or a
|
|
1042
|
-
// pasted URL be recognised and revoked on sight.
|
|
1043
|
+
// pasted URL be recognised and revoked on sight. The OAuth access token
|
|
1044
|
+
// was this clause's one standing exception until 2026-08-14 — the
|
|
1045
|
+
// authorization server it was born on had no mint hook to give it a
|
|
1046
|
+
// prefix, and its successor does.
|
|
1043
1047
|
//
|
|
1044
1048
|
// 3. NO PREFIX IS A PREFIX OF ANOTHER. This is what makes the dispatch
|
|
1045
1049
|
// order-independent, and it is the reason the populations are named on
|
|
@@ -1104,6 +1108,39 @@ export const DEPLOY_TOKEN = {
|
|
|
1104
1108
|
/** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 39`). */
|
|
1105
1109
|
TOTAL_LENGTH: 39,
|
|
1106
1110
|
};
|
|
1111
|
+
/**
|
|
1112
|
+
* Shape constants for OAuth access tokens (`oauth-{32 hex chars}`) — the
|
|
1113
|
+
* delegated population, minted by the platform's own authorization server
|
|
1114
|
+
* for a connected app acting on a user's behalf.
|
|
1115
|
+
*
|
|
1116
|
+
* Same width as the other two, and for the same reason: one entropy standard
|
|
1117
|
+
* across the platform, so "how long is a credential" has one answer.
|
|
1118
|
+
*
|
|
1119
|
+
* **This population is the access token alone.** Refresh tokens, authorization
|
|
1120
|
+
* codes and client secrets are deliberately NOT here and are deliberately not
|
|
1121
|
+
* prefixed by this constant: none of them ever enters the `Authorization:
|
|
1122
|
+
* Bearer` slot — a refresh token is posted as a form field to the token
|
|
1123
|
+
* endpoint, which knows what it is receiving — so `classifyToken` never sees
|
|
1124
|
+
* one and a prefix would name a population no dispatcher dispatches. The same
|
|
1125
|
+
* reasoning that keeps the deployment claim code bare.
|
|
1126
|
+
*
|
|
1127
|
+
* **The prefix must be applied at the MINT, never as a display wrapper.** The
|
|
1128
|
+
* authorization server hashes what it stores and the API hashes what it is
|
|
1129
|
+
* presented, so the prefix has to be inside the hashed string on both sides.
|
|
1130
|
+
* `@better-auth/oauth-provider` offers a `prefix.opaqueAccessToken` option
|
|
1131
|
+
* that prepends AFTER hashing and strips on its own read paths; using it would
|
|
1132
|
+
* store a hash of the UNPREFIXED token and silently break the platform's read
|
|
1133
|
+
* arm. The API therefore mints through `generateOpaqueAccessToken` — recorded
|
|
1134
|
+
* beside the config in `cloudflare/api/src/lib/auth/instance.ts`.
|
|
1135
|
+
*/
|
|
1136
|
+
export const OAUTH_TOKEN = {
|
|
1137
|
+
/** Prefix that identifies an OAuth access token. */
|
|
1138
|
+
PREFIX: 'oauth-',
|
|
1139
|
+
/** Number of hex characters following the prefix. */
|
|
1140
|
+
HEX_LENGTH: 32,
|
|
1141
|
+
/** Total length including prefix (`PREFIX.length + HEX_LENGTH = 38`). */
|
|
1142
|
+
TOTAL_LENGTH: 38,
|
|
1143
|
+
};
|
|
1107
1144
|
/**
|
|
1108
1145
|
* Shape constants for caller identifiers (the `X-Caller` instance-identity
|
|
1109
1146
|
* header — rate-limit bucketing for multi-tenant orchestrators). The API
|
|
@@ -1124,16 +1161,23 @@ export const CALLER = {
|
|
|
1124
1161
|
* client token in one wire slot (`Authorization: Bearer <value>`) and
|
|
1125
1162
|
* classifies by value, never by a side channel — this is the classifier.
|
|
1126
1163
|
*
|
|
1127
|
-
* `API_KEY` and `
|
|
1128
|
-
* `AuthMethod.TOKEN` — the equality is structural, so
|
|
1129
|
-
* straight into an auth method and the
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1164
|
+
* `API_KEY`, `DEPLOY_TOKEN` and `OAUTH` *are* `AuthMethod.API_KEY`,
|
|
1165
|
+
* `AuthMethod.TOKEN` and `AuthMethod.OAUTH` — the equality is structural, so
|
|
1166
|
+
* a classification flows straight into an auth method and the trio can never
|
|
1167
|
+
* drift.
|
|
1168
|
+
*
|
|
1169
|
+
* `OPAQUE` is any other value, and since 2026-08-14 it names NO population:
|
|
1170
|
+
* every credential this platform mints for the Bearer slot carries a prefix,
|
|
1171
|
+
* so an opaque bearer is a bearer we did not mint. It stays a member rather
|
|
1172
|
+
* than becoming a `null` return because a dispatcher with a total codomain
|
|
1173
|
+
* reads better than one with an absence in it — and because it is where a
|
|
1174
|
+
* future population would land before anyone gave it a shape, which is
|
|
1175
|
+
* exactly what the OAuth token itself did until its prefix existed.
|
|
1133
1176
|
*/
|
|
1134
1177
|
export const TokenKind = {
|
|
1135
1178
|
API_KEY: AuthMethod.API_KEY,
|
|
1136
1179
|
DEPLOY_TOKEN: AuthMethod.TOKEN,
|
|
1180
|
+
OAUTH: AuthMethod.OAUTH,
|
|
1137
1181
|
OPAQUE: 'opaque',
|
|
1138
1182
|
};
|
|
1139
1183
|
/**
|
|
@@ -1147,8 +1191,53 @@ export function classifyToken(token) {
|
|
|
1147
1191
|
return TokenKind.API_KEY;
|
|
1148
1192
|
if (token.startsWith(DEPLOY_TOKEN.PREFIX))
|
|
1149
1193
|
return TokenKind.DEPLOY_TOKEN;
|
|
1194
|
+
if (token.startsWith(OAUTH_TOKEN.PREFIX))
|
|
1195
|
+
return TokenKind.OAUTH;
|
|
1150
1196
|
return TokenKind.OPAQUE;
|
|
1151
1197
|
}
|
|
1198
|
+
/** The auth-scheme, lowercased — the form the comparison is made in. */
|
|
1199
|
+
const BEARER_SCHEME = 'bearer ';
|
|
1200
|
+
/**
|
|
1201
|
+
* Read the credential out of an `Authorization` header value — the step
|
|
1202
|
+
* BEFORE `classifyToken`, and the other half of the one wire slot this
|
|
1203
|
+
* section owns.
|
|
1204
|
+
*
|
|
1205
|
+
* Returns the credential's own bytes, or `null` when the header carries a
|
|
1206
|
+
* foreign scheme or nothing after the scheme.
|
|
1207
|
+
*
|
|
1208
|
+
* **The scheme is folded; the credential is not.** RFC 7235 §2.1 makes the
|
|
1209
|
+
* auth-scheme case-insensitive, so `bearer`, `Bearer` and `BEARER` are the
|
|
1210
|
+
* same header. The value after it is opaque and is compared literally
|
|
1211
|
+
* everywhere it is used — `ship-`/`deploy-`/`oauth-` are lowercase hex, and
|
|
1212
|
+
* folding them would make a credential match values it is not.
|
|
1213
|
+
*
|
|
1214
|
+
* **This platform has paid for the rule twice, which is why it has an owner
|
|
1215
|
+
* rather than a convention.** A spec-conformant `bearer ship-…` client was
|
|
1216
|
+
* refused for as long as the API's scheme test was spelled case-sensitively;
|
|
1217
|
+
* and `@better-auth/oauth-provider` carries the same defect in four places
|
|
1218
|
+
* today (`startsWith("Bearer ")`), which is precisely why the platform folds
|
|
1219
|
+
* the scheme itself and hands the provider a bare token.
|
|
1220
|
+
*
|
|
1221
|
+
* **ABSENCE is deliberately not this function's business.** A missing header
|
|
1222
|
+
* and an unreadable one are different facts, and the callers that care split
|
|
1223
|
+
* on them: the API worker's middleware distinguishes `absent` (the only
|
|
1224
|
+
* anonymous path) from `unreadable` (a presented credential that is refused),
|
|
1225
|
+
* and collapsing the two here would take that distinction away from the layer
|
|
1226
|
+
* that needs it. Callers check for the header themselves and pass its value.
|
|
1227
|
+
*
|
|
1228
|
+
* **Why this lives in the constitution rather than in a worker's `shared/`.**
|
|
1229
|
+
* It is the same wire boundary `classifyToken` already owns — one reads the
|
|
1230
|
+
* slot, the other dispatches on what came out — and a rule with two holders
|
|
1231
|
+
* whose drift is silent earns exactly one owner regardless of what the
|
|
1232
|
+
* convoy costs. The estate's recorded refusal to own a `Bearer` CONSTANT
|
|
1233
|
+
* stands and is a different thing: that is RFC vocabulary, the same reason
|
|
1234
|
+
* this package owns no `"POST"`. A parser is not a spelling.
|
|
1235
|
+
*/
|
|
1236
|
+
export function readBearerValue(header) {
|
|
1237
|
+
if (header.slice(0, BEARER_SCHEME.length).toLowerCase() !== BEARER_SCHEME)
|
|
1238
|
+
return null;
|
|
1239
|
+
return header.slice(BEARER_SCHEME.length) || null;
|
|
1240
|
+
}
|
|
1152
1241
|
/**
|
|
1153
1242
|
* OAuth scope vocabulary for delegated third-party access tokens.
|
|
1154
1243
|
* Single source of truth used by the authorization server (advertised in
|
|
@@ -1273,11 +1362,24 @@ export function validateApiKey(apiKey) {
|
|
|
1273
1362
|
export function validateDeployToken(deployToken) {
|
|
1274
1363
|
validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
|
|
1275
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Validate OAuth access token format
|
|
1367
|
+
*/
|
|
1368
|
+
export function validateOAuthToken(oauthToken) {
|
|
1369
|
+
validatePrefixedCredential(oauthToken, OAUTH_TOKEN, 'OAuth access token');
|
|
1370
|
+
}
|
|
1276
1371
|
/**
|
|
1277
1372
|
* Validate a client token of any population. Classifies by shape and applies
|
|
1278
|
-
* the matching format rules:
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1373
|
+
* the matching format rules: all three prefixed populations are validated
|
|
1374
|
+
* strictly; an OPAQUE token only needs to be non-empty.
|
|
1375
|
+
*
|
|
1376
|
+
* **The OPAQUE arm stays permissive on purpose**, even though the platform no
|
|
1377
|
+
* longer mints an unprefixed credential. It is the fallback for a population
|
|
1378
|
+
* that does not exist yet, and a client refusing a shape the server would
|
|
1379
|
+
* accept is the one failure mode this boundary must never have — the server
|
|
1380
|
+
* decides, and it refuses an unrecognised bearer anyway. Unprefixed OAuth
|
|
1381
|
+
* tokens from before 2026-08-14 land here and are refused server-side, which
|
|
1382
|
+
* is correct: they were revoked by the change, not grandfathered.
|
|
1281
1383
|
*/
|
|
1282
1384
|
export function validateToken(token) {
|
|
1283
1385
|
switch (classifyToken(token)) {
|
|
@@ -1287,6 +1389,9 @@ export function validateToken(token) {
|
|
|
1287
1389
|
case TokenKind.DEPLOY_TOKEN:
|
|
1288
1390
|
validateDeployToken(token);
|
|
1289
1391
|
return;
|
|
1392
|
+
case TokenKind.OAUTH:
|
|
1393
|
+
validateOAuthToken(token);
|
|
1394
|
+
return;
|
|
1290
1395
|
case TokenKind.OPAQUE:
|
|
1291
1396
|
if (!token)
|
|
1292
1397
|
throw ShipError.validation('Token must be a non-empty string');
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1697,12 +1697,13 @@ export interface PingResponse {
|
|
|
1697
1697
|
// =============================================================================
|
|
1698
1698
|
// The one address for credential vocabulary: where human identity lives
|
|
1699
1699
|
// (AUTH_BASE_PATH), how a request is authorized (AuthMethod), the shapes
|
|
1700
|
-
// that distinguish populations on the wire (API_KEY, DEPLOY_TOKEN,
|
|
1701
|
-
// the
|
|
1702
|
-
//
|
|
1700
|
+
// that distinguish populations on the wire (API_KEY, DEPLOY_TOKEN,
|
|
1701
|
+
// OAUTH_TOKEN, CALLER), the two halves of the one Bearer slot
|
|
1702
|
+
// (readBearerValue reads it, classifyToken/TokenKind dispatch on what came
|
|
1703
|
+
// out), and the delegated-access scopes (OAuthScope).
|
|
1703
1704
|
//
|
|
1704
1705
|
// THE SHAPE LAW, in three clauses, over the `Authorization: Bearer` slot's
|
|
1705
|
-
//
|
|
1706
|
+
// three populations below. The deployment claim code is the API's own
|
|
1706
1707
|
// (`AUTH.CLAIM`, server-side: the API mints it and the API validates it, so
|
|
1707
1708
|
// it has one holder and stays there) and shares only clause 1 — it is the
|
|
1708
1709
|
// platform's one deliberately BARE secret, because it never enters the
|
|
@@ -1719,9 +1720,12 @@ export interface PingResponse {
|
|
|
1719
1720
|
//
|
|
1720
1721
|
// 2. EVERY BEARER POPULATION IS NAMED BY ITS PREFIX. A credential says what
|
|
1721
1722
|
// it is before anything parses it — which is what lets `classifyToken`
|
|
1722
|
-
// below dispatch
|
|
1723
|
+
// below dispatch three populations sharing one `Authorization: Bearer`
|
|
1723
1724
|
// slot, and what lets a value found in a log, a support ticket or a
|
|
1724
|
-
// pasted URL be recognised and revoked on sight.
|
|
1725
|
+
// pasted URL be recognised and revoked on sight. The OAuth access token
|
|
1726
|
+
// was this clause's one standing exception until 2026-08-14 — the
|
|
1727
|
+
// authorization server it was born on had no mint hook to give it a
|
|
1728
|
+
// prefix, and its successor does.
|
|
1725
1729
|
//
|
|
1726
1730
|
// 3. NO PREFIX IS A PREFIX OF ANOTHER. This is what makes the dispatch
|
|
1727
1731
|
// order-independent, and it is the reason the populations are named on
|
|
@@ -1793,6 +1797,40 @@ export const DEPLOY_TOKEN = {
|
|
|
1793
1797
|
TOTAL_LENGTH: 39,
|
|
1794
1798
|
} as const;
|
|
1795
1799
|
|
|
1800
|
+
/**
|
|
1801
|
+
* Shape constants for OAuth access tokens (`oauth-{32 hex chars}`) — the
|
|
1802
|
+
* delegated population, minted by the platform's own authorization server
|
|
1803
|
+
* for a connected app acting on a user's behalf.
|
|
1804
|
+
*
|
|
1805
|
+
* Same width as the other two, and for the same reason: one entropy standard
|
|
1806
|
+
* across the platform, so "how long is a credential" has one answer.
|
|
1807
|
+
*
|
|
1808
|
+
* **This population is the access token alone.** Refresh tokens, authorization
|
|
1809
|
+
* codes and client secrets are deliberately NOT here and are deliberately not
|
|
1810
|
+
* prefixed by this constant: none of them ever enters the `Authorization:
|
|
1811
|
+
* Bearer` slot — a refresh token is posted as a form field to the token
|
|
1812
|
+
* endpoint, which knows what it is receiving — so `classifyToken` never sees
|
|
1813
|
+
* one and a prefix would name a population no dispatcher dispatches. The same
|
|
1814
|
+
* reasoning that keeps the deployment claim code bare.
|
|
1815
|
+
*
|
|
1816
|
+
* **The prefix must be applied at the MINT, never as a display wrapper.** The
|
|
1817
|
+
* authorization server hashes what it stores and the API hashes what it is
|
|
1818
|
+
* presented, so the prefix has to be inside the hashed string on both sides.
|
|
1819
|
+
* `@better-auth/oauth-provider` offers a `prefix.opaqueAccessToken` option
|
|
1820
|
+
* that prepends AFTER hashing and strips on its own read paths; using it would
|
|
1821
|
+
* store a hash of the UNPREFIXED token and silently break the platform's read
|
|
1822
|
+
* arm. The API therefore mints through `generateOpaqueAccessToken` — recorded
|
|
1823
|
+
* beside the config in `cloudflare/api/src/lib/auth/instance.ts`.
|
|
1824
|
+
*/
|
|
1825
|
+
export const OAUTH_TOKEN = {
|
|
1826
|
+
/** Prefix that identifies an OAuth access token. */
|
|
1827
|
+
PREFIX: 'oauth-',
|
|
1828
|
+
/** Number of hex characters following the prefix. */
|
|
1829
|
+
HEX_LENGTH: 32,
|
|
1830
|
+
/** Total length including prefix (`PREFIX.length + HEX_LENGTH = 38`). */
|
|
1831
|
+
TOTAL_LENGTH: 38,
|
|
1832
|
+
} as const;
|
|
1833
|
+
|
|
1796
1834
|
/**
|
|
1797
1835
|
* Shape constants for caller identifiers (the `X-Caller` instance-identity
|
|
1798
1836
|
* header — rate-limit bucketing for multi-tenant orchestrators). The API
|
|
@@ -1814,16 +1852,23 @@ export const CALLER = {
|
|
|
1814
1852
|
* client token in one wire slot (`Authorization: Bearer <value>`) and
|
|
1815
1853
|
* classifies by value, never by a side channel — this is the classifier.
|
|
1816
1854
|
*
|
|
1817
|
-
* `API_KEY` and `
|
|
1818
|
-
* `AuthMethod.TOKEN` — the equality is structural, so
|
|
1819
|
-
* straight into an auth method and the
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1855
|
+
* `API_KEY`, `DEPLOY_TOKEN` and `OAUTH` *are* `AuthMethod.API_KEY`,
|
|
1856
|
+
* `AuthMethod.TOKEN` and `AuthMethod.OAUTH` — the equality is structural, so
|
|
1857
|
+
* a classification flows straight into an auth method and the trio can never
|
|
1858
|
+
* drift.
|
|
1859
|
+
*
|
|
1860
|
+
* `OPAQUE` is any other value, and since 2026-08-14 it names NO population:
|
|
1861
|
+
* every credential this platform mints for the Bearer slot carries a prefix,
|
|
1862
|
+
* so an opaque bearer is a bearer we did not mint. It stays a member rather
|
|
1863
|
+
* than becoming a `null` return because a dispatcher with a total codomain
|
|
1864
|
+
* reads better than one with an absence in it — and because it is where a
|
|
1865
|
+
* future population would land before anyone gave it a shape, which is
|
|
1866
|
+
* exactly what the OAuth token itself did until its prefix existed.
|
|
1823
1867
|
*/
|
|
1824
1868
|
export const TokenKind = {
|
|
1825
1869
|
API_KEY: AuthMethod.API_KEY,
|
|
1826
1870
|
DEPLOY_TOKEN: AuthMethod.TOKEN,
|
|
1871
|
+
OAUTH: AuthMethod.OAUTH,
|
|
1827
1872
|
OPAQUE: 'opaque',
|
|
1828
1873
|
} as const;
|
|
1829
1874
|
|
|
@@ -1838,9 +1883,54 @@ export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
|
|
|
1838
1883
|
export function classifyToken(token: string): TokenKindType {
|
|
1839
1884
|
if (token.startsWith(API_KEY.PREFIX)) return TokenKind.API_KEY;
|
|
1840
1885
|
if (token.startsWith(DEPLOY_TOKEN.PREFIX)) return TokenKind.DEPLOY_TOKEN;
|
|
1886
|
+
if (token.startsWith(OAUTH_TOKEN.PREFIX)) return TokenKind.OAUTH;
|
|
1841
1887
|
return TokenKind.OPAQUE;
|
|
1842
1888
|
}
|
|
1843
1889
|
|
|
1890
|
+
/** The auth-scheme, lowercased — the form the comparison is made in. */
|
|
1891
|
+
const BEARER_SCHEME = 'bearer ';
|
|
1892
|
+
|
|
1893
|
+
/**
|
|
1894
|
+
* Read the credential out of an `Authorization` header value — the step
|
|
1895
|
+
* BEFORE `classifyToken`, and the other half of the one wire slot this
|
|
1896
|
+
* section owns.
|
|
1897
|
+
*
|
|
1898
|
+
* Returns the credential's own bytes, or `null` when the header carries a
|
|
1899
|
+
* foreign scheme or nothing after the scheme.
|
|
1900
|
+
*
|
|
1901
|
+
* **The scheme is folded; the credential is not.** RFC 7235 §2.1 makes the
|
|
1902
|
+
* auth-scheme case-insensitive, so `bearer`, `Bearer` and `BEARER` are the
|
|
1903
|
+
* same header. The value after it is opaque and is compared literally
|
|
1904
|
+
* everywhere it is used — `ship-`/`deploy-`/`oauth-` are lowercase hex, and
|
|
1905
|
+
* folding them would make a credential match values it is not.
|
|
1906
|
+
*
|
|
1907
|
+
* **This platform has paid for the rule twice, which is why it has an owner
|
|
1908
|
+
* rather than a convention.** A spec-conformant `bearer ship-…` client was
|
|
1909
|
+
* refused for as long as the API's scheme test was spelled case-sensitively;
|
|
1910
|
+
* and `@better-auth/oauth-provider` carries the same defect in four places
|
|
1911
|
+
* today (`startsWith("Bearer ")`), which is precisely why the platform folds
|
|
1912
|
+
* the scheme itself and hands the provider a bare token.
|
|
1913
|
+
*
|
|
1914
|
+
* **ABSENCE is deliberately not this function's business.** A missing header
|
|
1915
|
+
* and an unreadable one are different facts, and the callers that care split
|
|
1916
|
+
* on them: the API worker's middleware distinguishes `absent` (the only
|
|
1917
|
+
* anonymous path) from `unreadable` (a presented credential that is refused),
|
|
1918
|
+
* and collapsing the two here would take that distinction away from the layer
|
|
1919
|
+
* that needs it. Callers check for the header themselves and pass its value.
|
|
1920
|
+
*
|
|
1921
|
+
* **Why this lives in the constitution rather than in a worker's `shared/`.**
|
|
1922
|
+
* It is the same wire boundary `classifyToken` already owns — one reads the
|
|
1923
|
+
* slot, the other dispatches on what came out — and a rule with two holders
|
|
1924
|
+
* whose drift is silent earns exactly one owner regardless of what the
|
|
1925
|
+
* convoy costs. The estate's recorded refusal to own a `Bearer` CONSTANT
|
|
1926
|
+
* stands and is a different thing: that is RFC vocabulary, the same reason
|
|
1927
|
+
* this package owns no `"POST"`. A parser is not a spelling.
|
|
1928
|
+
*/
|
|
1929
|
+
export function readBearerValue(header: string): string | null {
|
|
1930
|
+
if (header.slice(0, BEARER_SCHEME.length).toLowerCase() !== BEARER_SCHEME) return null;
|
|
1931
|
+
return header.slice(BEARER_SCHEME.length) || null;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1844
1934
|
/**
|
|
1845
1935
|
* OAuth scope vocabulary for delegated third-party access tokens.
|
|
1846
1936
|
* Single source of truth used by the authorization server (advertised in
|
|
@@ -1988,11 +2078,25 @@ export function validateDeployToken(deployToken: string): void {
|
|
|
1988
2078
|
validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
|
|
1989
2079
|
}
|
|
1990
2080
|
|
|
2081
|
+
/**
|
|
2082
|
+
* Validate OAuth access token format
|
|
2083
|
+
*/
|
|
2084
|
+
export function validateOAuthToken(oauthToken: string): void {
|
|
2085
|
+
validatePrefixedCredential(oauthToken, OAUTH_TOKEN, 'OAuth access token');
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1991
2088
|
/**
|
|
1992
2089
|
* Validate a client token of any population. Classifies by shape and applies
|
|
1993
|
-
* the matching format rules:
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
2090
|
+
* the matching format rules: all three prefixed populations are validated
|
|
2091
|
+
* strictly; an OPAQUE token only needs to be non-empty.
|
|
2092
|
+
*
|
|
2093
|
+
* **The OPAQUE arm stays permissive on purpose**, even though the platform no
|
|
2094
|
+
* longer mints an unprefixed credential. It is the fallback for a population
|
|
2095
|
+
* that does not exist yet, and a client refusing a shape the server would
|
|
2096
|
+
* accept is the one failure mode this boundary must never have — the server
|
|
2097
|
+
* decides, and it refuses an unrecognised bearer anyway. Unprefixed OAuth
|
|
2098
|
+
* tokens from before 2026-08-14 land here and are refused server-side, which
|
|
2099
|
+
* is correct: they were revoked by the change, not grandfathered.
|
|
1996
2100
|
*/
|
|
1997
2101
|
export function validateToken(token: string): void {
|
|
1998
2102
|
switch (classifyToken(token)) {
|
|
@@ -2002,6 +2106,9 @@ export function validateToken(token: string): void {
|
|
|
2002
2106
|
case TokenKind.DEPLOY_TOKEN:
|
|
2003
2107
|
validateDeployToken(token);
|
|
2004
2108
|
return;
|
|
2109
|
+
case TokenKind.OAUTH:
|
|
2110
|
+
validateOAuthToken(token);
|
|
2111
|
+
return;
|
|
2005
2112
|
case TokenKind.OPAQUE:
|
|
2006
2113
|
if (!token) throw ShipError.validation('Token must be a non-empty string');
|
|
2007
2114
|
}
|