@spfn/auth 0.2.0-beta.88 → 0.2.0-beta.89
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 +53 -2
- package/dist/{authenticate-ZssfqJ7d.d.ts → authenticate-DK-hLSOW.d.ts} +16 -8
- package/dist/client-proof.d.ts +121 -1
- package/dist/client-proof.js +336 -5
- package/dist/client-proof.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/server.d.ts +79 -79
- package/dist/server.js +16 -4
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,11 +213,24 @@ cut off anything they no longer recognise.
|
|
|
213
213
|
|
|
214
214
|
```typescript
|
|
215
215
|
const { keys } = await authApi.listKeys.call({ body: {} });
|
|
216
|
-
// → [{ keyId, deviceName?, platform?, algorithm, fingerprintPrefix,
|
|
217
|
-
//
|
|
216
|
+
// → [{ keyId, deviceName?, platform?, algorithm, fingerprintPrefix, createdAtMillis,
|
|
217
|
+
// lastUsedAtMillis?, expiresAtMillis?, isExpired, isActive, revokedAtMillis? }]
|
|
218
218
|
|
|
219
219
|
await authApi.listKeys.call({ body: { includeRevoked: true } }); // also what was cut off
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Every moment is epoch milliseconds, not an ISO string — one representation across the whole
|
|
223
|
+
surface, so a generated Swift or Kotlin client reads an integer instead of choosing a date
|
|
224
|
+
formatter. This changed in mobile contract 0.5.0; an app still reading `createdAt` moves to
|
|
225
|
+
`createdAtMillis`.
|
|
220
226
|
|
|
227
|
+
`algorithm` is the `KeyAlgorithm` enum from contract 0.6.0 rather than a bare string — the routes
|
|
228
|
+
have always constrained it to those values, and the contract had been understating the server. The
|
|
229
|
+
declared values are the ones the server accepts and sends **now**: one can be added, and one can be
|
|
230
|
+
withdrawn for a weakness found later, so a generated client should be built to meet a value it does
|
|
231
|
+
not recognise rather than assume the set is closed.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
221
234
|
await authApi.revokeKey.call({ body: { keyId } }); // → { keyId, selfRevoked }
|
|
222
235
|
await authApi.revokeAllKeys.call({ body: {} }); // other devices only
|
|
223
236
|
await authApi.revokeAllKeys.call({ body: { includeCurrent: true } }); // everything
|
|
@@ -853,6 +866,44 @@ HTTP status).
|
|
|
853
866
|
construction or through the `/control/register-key` hook; the private half never reaches
|
|
854
867
|
the server. No persistence — a production enrollment/rotation story is phase 2.
|
|
855
868
|
|
|
869
|
+
### The contract version on the wire (contract 0.6.0)
|
|
870
|
+
|
|
871
|
+
A client compiled and shipped separately from the server cannot be fixed by redeploying. Until
|
|
872
|
+
0.6.0 a mismatch between what that client was generated against and what the server serves
|
|
873
|
+
surfaced as an undecodable body: the app looked broken and nothing said why.
|
|
874
|
+
|
|
875
|
+
Both ends now say what they are.
|
|
876
|
+
|
|
877
|
+
| Header | Direction | Sent by |
|
|
878
|
+
|--------|-----------|---------|
|
|
879
|
+
| `x-spfn-client-kind` | request | every client — `web`, `ios` or `android` |
|
|
880
|
+
| `x-spfn-client-version` | request | the client's own release: a store version, or a bundle build |
|
|
881
|
+
| `x-spfn-client-contract-version` | request | `ios` and `android` only |
|
|
882
|
+
| `x-spfn-server-contract-version` | response | the server, on every response including a refusal |
|
|
883
|
+
| `x-spfn-supported-contract-range` | response | the server, likewise |
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
import { createClientVersionMiddleware } from '@spfn/auth/client-proof';
|
|
887
|
+
|
|
888
|
+
// Mount before authentication: enrollment and login carry no proof, and they are
|
|
889
|
+
// where a stale client arrives first.
|
|
890
|
+
app.use('*', createClientVersionMiddleware());
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
- **`web` states no contract version**, because a browser bundle is deployed with the server that
|
|
894
|
+
serves it and has no second version to reconcile. It is exempt by construction, not by leniency.
|
|
895
|
+
- **An `ios` or `android` client that states no contract version, or one outside the range, is
|
|
896
|
+
refused** `CONTRACT_UNSUPPORTED` (409) with the usual envelope.
|
|
897
|
+
- **A request naming no kind passes** — a curl, a health probe, a server-to-server call is not a
|
|
898
|
+
deployed client this rule is about.
|
|
899
|
+
- **None of it enters the proof input.** These are diagnostic; `PROOF_INPUT_FIELDS` is unchanged.
|
|
900
|
+
- **The server states facts and stops there.** Comparing the announced range against its own version
|
|
901
|
+
and deciding a user should see an update prompt is the client's judgment, made in the client. The
|
|
902
|
+
server has no way to make an app update and does not pretend to.
|
|
903
|
+
|
|
904
|
+
Response header names are deliberately distinct from the request ones: a proxy that echoes a request
|
|
905
|
+
header into the response would otherwise make the client's own version look like the server's.
|
|
906
|
+
|
|
856
907
|
### Usage — dev surface (mobile integration target)
|
|
857
908
|
|
|
858
909
|
The fastest path: run the packaged dev handler, which already serves the three contract
|
|
@@ -270,15 +270,23 @@ interface KeySummary {
|
|
|
270
270
|
algorithm: KeyAlgorithmType;
|
|
271
271
|
/** First bytes of the fingerprint — enough to tell two entries apart. */
|
|
272
272
|
fingerprintPrefix: string;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Milliseconds since the Unix epoch, not an ISO string.
|
|
275
|
+
*
|
|
276
|
+
* One representation of a moment across the whole surface: a generated Swift
|
|
277
|
+
* or Kotlin client reads an integer with no date formatter, and
|
|
278
|
+
* `ISO8601DateFormatter` rejecting fractional seconds by default stops being
|
|
279
|
+
* a way for the two SDKs to disagree about the same value.
|
|
280
|
+
*/
|
|
281
|
+
createdAtMillis: number;
|
|
282
|
+
lastUsedAtMillis?: number;
|
|
283
|
+
expiresAtMillis?: number;
|
|
276
284
|
/** The TTL has run out. The key still reads as active; authenticate refuses it. */
|
|
277
285
|
isExpired: boolean;
|
|
278
286
|
/** False once revoked. Only ever false when the caller asked for revoked keys. */
|
|
279
287
|
isActive: boolean;
|
|
280
288
|
/** When it was revoked, for the "what did I cut off, and when" reading. */
|
|
281
|
-
|
|
289
|
+
revokedAtMillis?: number;
|
|
282
290
|
}
|
|
283
291
|
interface ListKeysParams {
|
|
284
292
|
userId: number;
|
|
@@ -871,7 +879,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
|
|
|
871
879
|
id: number;
|
|
872
880
|
name: string;
|
|
873
881
|
displayName: string;
|
|
874
|
-
category: "
|
|
882
|
+
category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
|
|
875
883
|
}[];
|
|
876
884
|
userId: number;
|
|
877
885
|
publicId: string;
|
|
@@ -1171,8 +1179,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
|
|
|
1171
1179
|
}, {}, {
|
|
1172
1180
|
roles: {
|
|
1173
1181
|
description: string | null;
|
|
1174
|
-
id: number;
|
|
1175
1182
|
name: string;
|
|
1183
|
+
id: number;
|
|
1176
1184
|
displayName: string;
|
|
1177
1185
|
isBuiltin: boolean;
|
|
1178
1186
|
isSystem: boolean;
|
|
@@ -1193,8 +1201,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
|
|
|
1193
1201
|
}, {}, {
|
|
1194
1202
|
role: {
|
|
1195
1203
|
description: string | null;
|
|
1196
|
-
id: number;
|
|
1197
1204
|
name: string;
|
|
1205
|
+
id: number;
|
|
1198
1206
|
displayName: string;
|
|
1199
1207
|
isBuiltin: boolean;
|
|
1200
1208
|
isSystem: boolean;
|
|
@@ -1217,8 +1225,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
|
|
|
1217
1225
|
}, {}, {
|
|
1218
1226
|
role: {
|
|
1219
1227
|
description: string | null;
|
|
1220
|
-
id: number;
|
|
1221
1228
|
name: string;
|
|
1229
|
+
id: number;
|
|
1222
1230
|
displayName: string;
|
|
1223
1231
|
isBuiltin: boolean;
|
|
1224
1232
|
isSystem: boolean;
|
package/dist/client-proof.d.ts
CHANGED
|
@@ -149,6 +149,14 @@ declare class ClientProofRefusal {
|
|
|
149
149
|
static bodyNotTheDeclaredType(): ClientProofRefusal;
|
|
150
150
|
static sessionHeaderMisplaced(): ClientProofRefusal;
|
|
151
151
|
static unprocessable(): ClientProofRefusal;
|
|
152
|
+
/**
|
|
153
|
+
* A client that ships separately from the server said nothing about which
|
|
154
|
+
* contract it was built against. Without it the server cannot tell whether
|
|
155
|
+
* the two ends agree, and answering as though they do is what produces the
|
|
156
|
+
* undecodable body this check exists to replace.
|
|
157
|
+
*/
|
|
158
|
+
static contractVersionMissing(): ClientProofRefusal;
|
|
159
|
+
static contractVersionUnsupported(): ClientProofRefusal;
|
|
152
160
|
static profileRejected(): ClientProofRefusal;
|
|
153
161
|
static sessionRevoked(): ClientProofRefusal;
|
|
154
162
|
static proofExpired(): ClientProofRefusal;
|
|
@@ -554,4 +562,116 @@ interface ClientProofGuardOptions {
|
|
|
554
562
|
*/
|
|
555
563
|
declare function createClientProofGuard(state: ClientProofState, options?: ClientProofGuardOptions): MiddlewareHandler;
|
|
556
564
|
|
|
557
|
-
|
|
565
|
+
/**
|
|
566
|
+
* The header names each end announces itself under.
|
|
567
|
+
*
|
|
568
|
+
* Separated from the logic that reads them so the contract bundle can name them
|
|
569
|
+
* without importing the version comparison, which reads the bundle back. These
|
|
570
|
+
* are declarations and depend on nothing.
|
|
571
|
+
*
|
|
572
|
+
* @module server/client-proof/wire-headers
|
|
573
|
+
*/
|
|
574
|
+
/** What a client says about itself, one header each. */
|
|
575
|
+
declare const CLIENT_IDENTITY_HEADERS: {
|
|
576
|
+
readonly kind: "x-spfn-client-kind";
|
|
577
|
+
readonly version: "x-spfn-client-version";
|
|
578
|
+
readonly contractVersion: "x-spfn-client-contract-version";
|
|
579
|
+
};
|
|
580
|
+
/**
|
|
581
|
+
* What the server says about itself, on every response.
|
|
582
|
+
*
|
|
583
|
+
* Distinct names from the request headers on purpose: a proxy that echoes a
|
|
584
|
+
* request header into the response would otherwise make the client's own
|
|
585
|
+
* version look like the server's.
|
|
586
|
+
*/
|
|
587
|
+
declare const SERVER_CONTRACT_HEADERS: {
|
|
588
|
+
readonly version: "x-spfn-server-contract-version";
|
|
589
|
+
readonly supportedRange: "x-spfn-supported-contract-range";
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* The client kinds the server distinguishes.
|
|
593
|
+
*
|
|
594
|
+
* `web` is separated from the two app kinds because it carries no contract
|
|
595
|
+
* version: a browser bundle is deployed with the server that serves it, so
|
|
596
|
+
* there is no second version to reconcile.
|
|
597
|
+
*/
|
|
598
|
+
declare const CLIENT_KINDS: readonly ["web", "ios", "android"];
|
|
599
|
+
type ClientKind = typeof CLIENT_KINDS[number];
|
|
600
|
+
/** A kind that ships independently of the server, so its contract version matters. */
|
|
601
|
+
declare function isAppKind(kind: ClientKind): boolean;
|
|
602
|
+
|
|
603
|
+
/** What one request announced about the client that sent it. */
|
|
604
|
+
interface ClientIdentity {
|
|
605
|
+
kind: ClientKind;
|
|
606
|
+
/** The client's own release — a store version, or a bundle build. */
|
|
607
|
+
version: string | null;
|
|
608
|
+
/** The contract version the client was generated from. Never set for `web`. */
|
|
609
|
+
contractVersion: string | null;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Reads the identity headers, or null when the kind is absent or unrecognised.
|
|
613
|
+
*
|
|
614
|
+
* Null is not by itself a refusal — a request from something that predates
|
|
615
|
+
* these headers reaches here too. `judgeClientIdentity` decides.
|
|
616
|
+
*/
|
|
617
|
+
declare function readClientIdentity(headers: Headers): ClientIdentity | null;
|
|
618
|
+
/**
|
|
619
|
+
* Whether the server serves what the client was generated against.
|
|
620
|
+
*
|
|
621
|
+
* Under 0.x the minor carries breaking changes, so a supported client agrees on
|
|
622
|
+
* major and minor. From 1.0.0 the major alone decides. This is the rule
|
|
623
|
+
* `CONTRACT_SUPPORTED_RANGE` spells out; keeping it as a comparison rather than
|
|
624
|
+
* parsing that string leaves one place to change when the line reaches 1.0.0.
|
|
625
|
+
*/
|
|
626
|
+
declare function isContractVersionSupported(clientVersion: string): boolean;
|
|
627
|
+
/**
|
|
628
|
+
* The refusal a request's announced identity earns, or null to let it through.
|
|
629
|
+
*
|
|
630
|
+
* An app kind must state a contract version this server serves. A version it
|
|
631
|
+
* does not serve, and the absence of one, are the same answer: the two ends do
|
|
632
|
+
* not agree on what the contract is, which is what CONTRACT_UNSUPPORTED means.
|
|
633
|
+
* The response carries the server's version and range, so the client can say
|
|
634
|
+
* which way the gap runs.
|
|
635
|
+
*
|
|
636
|
+
* `web` is exempt from the contract check by construction, not by leniency.
|
|
637
|
+
*
|
|
638
|
+
* A request with no recognised kind passes. The check is on what a client says
|
|
639
|
+
* about itself, and a caller that says nothing — a curl, a health probe, a
|
|
640
|
+
* server-to-server call — is not a deployed client this rule is about.
|
|
641
|
+
*/
|
|
642
|
+
declare function judgeClientIdentity(identity: ClientIdentity | null): ClientProofRefusal | null;
|
|
643
|
+
/** Writes the server's own announcement onto a response's headers. */
|
|
644
|
+
declare function applyServerContractHeaders(headers: Headers): void;
|
|
645
|
+
/** The same announcement as a plain object, for a response built from one. */
|
|
646
|
+
declare function serverContractHeaders(): Record<string, string>;
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* The version announcement, applied to every request rather than to the proven
|
|
650
|
+
* ones.
|
|
651
|
+
*
|
|
652
|
+
* Enrollment and login are the first calls a client makes and they carry no
|
|
653
|
+
* proof — there is no key to sign with yet. A check that lives inside proof
|
|
654
|
+
* admission therefore never sees the client it is meant to catch: an outdated
|
|
655
|
+
* app fails at login, before it reaches anything proven. This runs ahead of all
|
|
656
|
+
* of it.
|
|
657
|
+
*
|
|
658
|
+
* hono is imported as types only, so this module adds no runtime dependency.
|
|
659
|
+
*
|
|
660
|
+
* @module server/client-proof/version-middleware
|
|
661
|
+
*/
|
|
662
|
+
|
|
663
|
+
/** The context key the identity is left under, for a handler that wants it. */
|
|
664
|
+
declare const CLIENT_IDENTITY_CONTEXT_KEY = "clientIdentity";
|
|
665
|
+
/**
|
|
666
|
+
* Announces the server's contract version on every response and refuses a
|
|
667
|
+
* client whose own contract version this server does not serve.
|
|
668
|
+
*
|
|
669
|
+
* The announcement goes out either way. A refused client needs it most — the
|
|
670
|
+
* refusal says the two ends disagree, and the range is what says how.
|
|
671
|
+
*
|
|
672
|
+
* Mount this before authentication, not after: the point is to answer a stale
|
|
673
|
+
* client before anything else has a chance to fail confusingly.
|
|
674
|
+
*/
|
|
675
|
+
declare function createClientVersionMiddleware(): MiddlewareHandler;
|
|
676
|
+
|
|
677
|
+
export { ABSENT_BODY_SHA256, AUTH_SURFACE_OPERATIONS, type Admission, CLIENT_IDENTITY_CONTEXT_KEY, CLIENT_IDENTITY_HEADERS, CLIENT_KINDS, CLIENT_PROOF_CONTENT_TYPE, CLIENT_PROOF_HEADERS, CLIENT_PROOF_PROFILE, CONTRACT_OPERATIONS, CONTROL_PREFIX, CONTROL_TOKEN_HEADER, CanonicalJsonError, type CanonicalJsonErrorCode, type CanonicalObject, type CanonicalValue, type ClientIdentity, type ClientKind, type ClientProofClock, type ClientProofContext, type ClientProofCredentials, type ClientProofDevHandler, type ClientProofDevHandlerOptions, type ClientProofErrorCode, type ClientProofGuardOptions, type ClientProofInput, ClientProofRefusal, type ClientProofReplayStore, ClientProofState, type ClientProofStateOptions, type ClientProofStats, type ContractItem, type ContractOperation, ContractTypeError, DEFAULT_REPLAY_WINDOW_MILLIS, DEFAULT_SESSION_TTL_MILLIS, DEV_CATALOGUE, DEV_MAX_LIMIT, type EchoRequest, type HandshakeRequest, type ListItemsRequest, MemoryReplayLedger, MemoryReplayStore, PROOF_SIGNATURE_BYTES, PROOF_SIGNATURE_HEX_LENGTH, ProofInputError, RedisReplayStore, SERVER_CONTRACT_HEADERS, TestClock, admitClientProofRequest, applyServerContractHeaders, canonicalProofInput, configureClientProofReplayStore, createClientProofDevHandler, createClientProofGuard, createClientVersionMiddleware, decodeEchoRequest, decodeHandshakeRequest, decodeListItemsRequest, encodeCanonicalJson, encodeEchoResponse, encodeHandshakeResponse, encodeListItemsResponse, getClientProofReplayStore, isAppKind, isCanonicalBytes, isContractVersionSupported, isRequestContentType, judgeClientIdentity, newHexId, parseCanonicalJson, parseClientProofPublicKey, readClientIdentity, readCredentials, replayLedgerKey, serverContractHeaders, sha256Hex, signClientProof, systemClock, verifyClientProof };
|
package/dist/client-proof.js
CHANGED
|
@@ -496,6 +496,18 @@ var ClientProofRefusal = class _ClientProofRefusal {
|
|
|
496
496
|
static unprocessable() {
|
|
497
497
|
return contractViolation("the request could not be processed");
|
|
498
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* A client that ships separately from the server said nothing about which
|
|
501
|
+
* contract it was built against. Without it the server cannot tell whether
|
|
502
|
+
* the two ends agree, and answering as though they do is what produces the
|
|
503
|
+
* undecodable body this check exists to replace.
|
|
504
|
+
*/
|
|
505
|
+
static contractVersionMissing() {
|
|
506
|
+
return contractViolation("a client of this kind must state the contract version it was generated from");
|
|
507
|
+
}
|
|
508
|
+
static contractVersionUnsupported() {
|
|
509
|
+
return contractViolation("the stated contract version is outside the range this server serves");
|
|
510
|
+
}
|
|
499
511
|
// ---- the profile allowlist ----------------------------------------------
|
|
500
512
|
static profileRejected() {
|
|
501
513
|
return new _ClientProofRefusal("PROFILE_REJECTED", "the named auth profile is not on this contract's allowlist");
|
|
@@ -1045,17 +1057,17 @@ function decodeListItemsRequest(value) {
|
|
|
1045
1057
|
}
|
|
1046
1058
|
return request;
|
|
1047
1059
|
}
|
|
1048
|
-
function objectWithKeys(value,
|
|
1060
|
+
function objectWithKeys(value, required2, optional2) {
|
|
1049
1061
|
if (!(value instanceof Map)) {
|
|
1050
1062
|
throw new ContractTypeError();
|
|
1051
1063
|
}
|
|
1052
|
-
for (const key of
|
|
1064
|
+
for (const key of required2) {
|
|
1053
1065
|
if (!value.has(key)) {
|
|
1054
1066
|
throw new ContractTypeError();
|
|
1055
1067
|
}
|
|
1056
1068
|
}
|
|
1057
1069
|
for (const key of value.keys()) {
|
|
1058
|
-
if (!
|
|
1070
|
+
if (!required2.includes(key) && !optional2.includes(key)) {
|
|
1059
1071
|
throw new ContractTypeError();
|
|
1060
1072
|
}
|
|
1061
1073
|
}
|
|
@@ -1251,6 +1263,289 @@ function answer(status, value) {
|
|
|
1251
1263
|
return new Response(buffer, { status, headers: { "content-type": "application/json" } });
|
|
1252
1264
|
}
|
|
1253
1265
|
|
|
1266
|
+
// src/server/client-proof/contract-bundle.ts
|
|
1267
|
+
import { createHash as createHash2 } from "crypto";
|
|
1268
|
+
|
|
1269
|
+
// src/server/types.ts
|
|
1270
|
+
var KEY_ALGORITHM = ["ES256", "RS256"];
|
|
1271
|
+
|
|
1272
|
+
// src/server/client-proof/wire-headers.ts
|
|
1273
|
+
var CLIENT_IDENTITY_HEADERS = {
|
|
1274
|
+
kind: "x-spfn-client-kind",
|
|
1275
|
+
version: "x-spfn-client-version",
|
|
1276
|
+
contractVersion: "x-spfn-client-contract-version"
|
|
1277
|
+
};
|
|
1278
|
+
var SERVER_CONTRACT_HEADERS = {
|
|
1279
|
+
version: "x-spfn-server-contract-version",
|
|
1280
|
+
supportedRange: "x-spfn-supported-contract-range"
|
|
1281
|
+
};
|
|
1282
|
+
var CLIENT_KINDS = ["web", "ios", "android"];
|
|
1283
|
+
function isAppKind(kind) {
|
|
1284
|
+
return kind !== "web";
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/server/client-proof/contract-bundle.ts
|
|
1288
|
+
var CONTRACT_VERSION = "0.6.0";
|
|
1289
|
+
var CONTRACT_MAJOR = 0;
|
|
1290
|
+
var CONTRACT_SUPPORTED_RANGE = ">=0.6.0 <0.7.0";
|
|
1291
|
+
function required(name, type) {
|
|
1292
|
+
return { name, type, optional: false };
|
|
1293
|
+
}
|
|
1294
|
+
function optional(name, type) {
|
|
1295
|
+
return { name, type, optional: true };
|
|
1296
|
+
}
|
|
1297
|
+
var CONTRACT_TYPES = [
|
|
1298
|
+
{
|
|
1299
|
+
name: "HandshakeRequest",
|
|
1300
|
+
fields: [
|
|
1301
|
+
required("clientId", "string"),
|
|
1302
|
+
required("keyId", "string"),
|
|
1303
|
+
required("nonce", "string"),
|
|
1304
|
+
required("issuedAtMillis", "integer")
|
|
1305
|
+
]
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
name: "HandshakeResponse",
|
|
1309
|
+
fields: [
|
|
1310
|
+
required("sessionId", "string"),
|
|
1311
|
+
required("expiresAtMillis", "integer")
|
|
1312
|
+
]
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
name: "EchoRequest",
|
|
1316
|
+
fields: [
|
|
1317
|
+
required("message", "string"),
|
|
1318
|
+
required("sequence", "integer")
|
|
1319
|
+
]
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
name: "EchoResponse",
|
|
1323
|
+
fields: [
|
|
1324
|
+
required("message", "string"),
|
|
1325
|
+
required("sequence", "integer"),
|
|
1326
|
+
required("serverTimeMillis", "integer")
|
|
1327
|
+
]
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
name: "ListItemsRequest",
|
|
1331
|
+
fields: [
|
|
1332
|
+
required("limit", "integer"),
|
|
1333
|
+
optional("cursor", "string")
|
|
1334
|
+
]
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
name: "Item",
|
|
1338
|
+
fields: [
|
|
1339
|
+
required("id", "string"),
|
|
1340
|
+
required("name", "string"),
|
|
1341
|
+
required("updatedAtMillis", "integer")
|
|
1342
|
+
]
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
name: "ListItemsResponse",
|
|
1346
|
+
fields: [
|
|
1347
|
+
required("items", "array<Item>"),
|
|
1348
|
+
optional("nextCursor", "string")
|
|
1349
|
+
]
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
name: "RegisterRequest",
|
|
1353
|
+
fields: [
|
|
1354
|
+
optional("email", "string"),
|
|
1355
|
+
optional("phone", "string"),
|
|
1356
|
+
required("verificationToken", "string"),
|
|
1357
|
+
required("password", "string"),
|
|
1358
|
+
required("publicKey", "string"),
|
|
1359
|
+
required("keyId", "string"),
|
|
1360
|
+
required("fingerprint", "string"),
|
|
1361
|
+
required("algorithm", "KeyAlgorithm")
|
|
1362
|
+
]
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
name: "RegisterResponse",
|
|
1366
|
+
fields: [
|
|
1367
|
+
required("userId", "string"),
|
|
1368
|
+
required("publicId", "string"),
|
|
1369
|
+
optional("email", "string"),
|
|
1370
|
+
optional("phone", "string")
|
|
1371
|
+
]
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
name: "LoginRequest",
|
|
1375
|
+
fields: [
|
|
1376
|
+
optional("email", "string"),
|
|
1377
|
+
optional("phone", "string"),
|
|
1378
|
+
required("password", "string"),
|
|
1379
|
+
required("publicKey", "string"),
|
|
1380
|
+
required("keyId", "string"),
|
|
1381
|
+
required("fingerprint", "string"),
|
|
1382
|
+
required("algorithm", "KeyAlgorithm"),
|
|
1383
|
+
optional("oldKeyId", "string")
|
|
1384
|
+
]
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
name: "LoginResponse",
|
|
1388
|
+
fields: [
|
|
1389
|
+
required("userId", "string"),
|
|
1390
|
+
required("publicId", "string"),
|
|
1391
|
+
optional("email", "string"),
|
|
1392
|
+
optional("phone", "string"),
|
|
1393
|
+
required("passwordChangeRequired", "boolean")
|
|
1394
|
+
]
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
name: "OauthNativeRequest",
|
|
1398
|
+
fields: [
|
|
1399
|
+
required("idToken", "string"),
|
|
1400
|
+
required("nonce", "string"),
|
|
1401
|
+
optional("accessToken", "string"),
|
|
1402
|
+
required("publicKey", "string"),
|
|
1403
|
+
required("keyId", "string"),
|
|
1404
|
+
required("fingerprint", "string"),
|
|
1405
|
+
required("algorithm", "KeyAlgorithm")
|
|
1406
|
+
]
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: "OauthNativeResponse",
|
|
1410
|
+
fields: [
|
|
1411
|
+
required("userId", "string"),
|
|
1412
|
+
required("keyId", "string"),
|
|
1413
|
+
required("isNewUser", "boolean")
|
|
1414
|
+
]
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
name: "RotateKeyRequest",
|
|
1418
|
+
fields: [
|
|
1419
|
+
required("publicKey", "string"),
|
|
1420
|
+
required("keyId", "string"),
|
|
1421
|
+
required("fingerprint", "string"),
|
|
1422
|
+
required("algorithm", "KeyAlgorithm")
|
|
1423
|
+
]
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
name: "RotateKeyResponse",
|
|
1427
|
+
fields: [
|
|
1428
|
+
required("success", "boolean"),
|
|
1429
|
+
required("keyId", "string")
|
|
1430
|
+
]
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
name: "ListKeysRequest",
|
|
1434
|
+
fields: [
|
|
1435
|
+
optional("includeRevoked", "boolean")
|
|
1436
|
+
]
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
name: "KeySummary",
|
|
1440
|
+
fields: [
|
|
1441
|
+
required("keyId", "string"),
|
|
1442
|
+
optional("deviceName", "string"),
|
|
1443
|
+
optional("platform", "string"),
|
|
1444
|
+
required("algorithm", "KeyAlgorithm"),
|
|
1445
|
+
required("fingerprintPrefix", "string"),
|
|
1446
|
+
required("createdAtMillis", "integer"),
|
|
1447
|
+
optional("lastUsedAtMillis", "integer"),
|
|
1448
|
+
optional("expiresAtMillis", "integer"),
|
|
1449
|
+
required("isExpired", "boolean"),
|
|
1450
|
+
required("isActive", "boolean"),
|
|
1451
|
+
optional("revokedAtMillis", "integer")
|
|
1452
|
+
]
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
name: "ListKeysResponse",
|
|
1456
|
+
fields: [
|
|
1457
|
+
required("keys", "array<KeySummary>")
|
|
1458
|
+
]
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
name: "RevokeKeyRequest",
|
|
1462
|
+
fields: [
|
|
1463
|
+
required("keyId", "string")
|
|
1464
|
+
]
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
name: "RevokeKeyResponse",
|
|
1468
|
+
fields: [
|
|
1469
|
+
required("keyId", "string"),
|
|
1470
|
+
required("selfRevoked", "boolean")
|
|
1471
|
+
]
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
name: "RevokeAllKeysRequest",
|
|
1475
|
+
fields: [
|
|
1476
|
+
optional("includeCurrent", "boolean")
|
|
1477
|
+
]
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
name: "RevokeAllKeysResponse",
|
|
1481
|
+
fields: [
|
|
1482
|
+
required("revokedCount", "integer"),
|
|
1483
|
+
required("currentKeyRevoked", "boolean")
|
|
1484
|
+
]
|
|
1485
|
+
}
|
|
1486
|
+
];
|
|
1487
|
+
var CONTRACT_ENUMS = [
|
|
1488
|
+
{ name: "KeyAlgorithm", values: [...KEY_ALGORITHM] }
|
|
1489
|
+
];
|
|
1490
|
+
var BUNDLE_FILENAME = "spfn-mobile-contract.json";
|
|
1491
|
+
var BUNDLE_REPO_PATH = `contracts/mobile/${BUNDLE_FILENAME}`;
|
|
1492
|
+
|
|
1493
|
+
// src/server/client-proof/wire-version.ts
|
|
1494
|
+
function readClientIdentity(headers) {
|
|
1495
|
+
const kind = headers.get(CLIENT_IDENTITY_HEADERS.kind);
|
|
1496
|
+
if (kind === null || !isClientKind(kind)) {
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
kind,
|
|
1501
|
+
version: headers.get(CLIENT_IDENTITY_HEADERS.version),
|
|
1502
|
+
contractVersion: headers.get(CLIENT_IDENTITY_HEADERS.contractVersion)
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
function isClientKind(value) {
|
|
1506
|
+
return CLIENT_KINDS.includes(value);
|
|
1507
|
+
}
|
|
1508
|
+
function isContractVersionSupported(clientVersion) {
|
|
1509
|
+
const client = parseVersion(clientVersion);
|
|
1510
|
+
if (client === null) {
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
const server = parseVersion(CONTRACT_VERSION);
|
|
1514
|
+
if (server === null || client.major !== server.major) {
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
return CONTRACT_MAJOR > 0 || client.minor === server.minor;
|
|
1518
|
+
}
|
|
1519
|
+
function parseVersion(raw) {
|
|
1520
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(raw);
|
|
1521
|
+
if (match === null) {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
return { major: Number(match[1]), minor: Number(match[2]) };
|
|
1525
|
+
}
|
|
1526
|
+
function judgeClientIdentity(identity) {
|
|
1527
|
+
if (identity === null || !isAppKind(identity.kind)) {
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
if (identity.contractVersion === null) {
|
|
1531
|
+
return ClientProofRefusal.contractVersionMissing();
|
|
1532
|
+
}
|
|
1533
|
+
if (!isContractVersionSupported(identity.contractVersion)) {
|
|
1534
|
+
return ClientProofRefusal.contractVersionUnsupported();
|
|
1535
|
+
}
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
function applyServerContractHeaders(headers) {
|
|
1539
|
+
headers.set(SERVER_CONTRACT_HEADERS.version, CONTRACT_VERSION);
|
|
1540
|
+
headers.set(SERVER_CONTRACT_HEADERS.supportedRange, CONTRACT_SUPPORTED_RANGE);
|
|
1541
|
+
}
|
|
1542
|
+
function serverContractHeaders() {
|
|
1543
|
+
return {
|
|
1544
|
+
[SERVER_CONTRACT_HEADERS.version]: CONTRACT_VERSION,
|
|
1545
|
+
[SERVER_CONTRACT_HEADERS.supportedRange]: CONTRACT_SUPPORTED_RANGE
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1254
1549
|
// src/server/client-proof/dev-handler.ts
|
|
1255
1550
|
var MAX_BODY_BYTES = 1 << 20;
|
|
1256
1551
|
var HTTP_OK2 = 200;
|
|
@@ -1368,7 +1663,7 @@ function listItems(request) {
|
|
|
1368
1663
|
function contractResponse(status, body) {
|
|
1369
1664
|
return new Response(toArrayBuffer(body), {
|
|
1370
1665
|
status,
|
|
1371
|
-
headers: { "content-type": "application/json" }
|
|
1666
|
+
headers: { "content-type": "application/json", ...serverContractHeaders() }
|
|
1372
1667
|
});
|
|
1373
1668
|
}
|
|
1374
1669
|
async function readBodyCapped(request) {
|
|
@@ -1419,7 +1714,8 @@ function createClientProofGuard(state, options = {}) {
|
|
|
1419
1714
|
const bytes = admission.refusal.envelopeBytes(newHexId());
|
|
1420
1715
|
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1421
1716
|
return c.newResponse(buffer, admission.refusal.httpStatus, {
|
|
1422
|
-
"content-type": "application/json"
|
|
1717
|
+
"content-type": "application/json",
|
|
1718
|
+
...serverContractHeaders()
|
|
1423
1719
|
});
|
|
1424
1720
|
}
|
|
1425
1721
|
c.set("clientType", "mobile");
|
|
@@ -1431,9 +1727,36 @@ function createClientProofGuard(state, options = {}) {
|
|
|
1431
1727
|
return void 0;
|
|
1432
1728
|
};
|
|
1433
1729
|
}
|
|
1730
|
+
|
|
1731
|
+
// src/server/client-proof/version-middleware.ts
|
|
1732
|
+
var CLIENT_IDENTITY_CONTEXT_KEY = "clientIdentity";
|
|
1733
|
+
function createClientVersionMiddleware() {
|
|
1734
|
+
return async (c, next) => {
|
|
1735
|
+
const identity = readClientIdentity(c.req.raw.headers);
|
|
1736
|
+
const refusal = judgeClientIdentity(identity);
|
|
1737
|
+
if (refusal !== null) {
|
|
1738
|
+
const bytes = refusal.envelopeBytes(newHexId());
|
|
1739
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1740
|
+
const response = c.newResponse(buffer, refusal.httpStatus, {
|
|
1741
|
+
"content-type": "application/json"
|
|
1742
|
+
});
|
|
1743
|
+
applyServerContractHeaders(response.headers);
|
|
1744
|
+
return response;
|
|
1745
|
+
}
|
|
1746
|
+
if (identity !== null) {
|
|
1747
|
+
c.set(CLIENT_IDENTITY_CONTEXT_KEY, identity);
|
|
1748
|
+
}
|
|
1749
|
+
await next();
|
|
1750
|
+
applyServerContractHeaders(c.res.headers);
|
|
1751
|
+
return void 0;
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1434
1754
|
export {
|
|
1435
1755
|
ABSENT_BODY_SHA256,
|
|
1436
1756
|
AUTH_SURFACE_OPERATIONS,
|
|
1757
|
+
CLIENT_IDENTITY_CONTEXT_KEY,
|
|
1758
|
+
CLIENT_IDENTITY_HEADERS,
|
|
1759
|
+
CLIENT_KINDS,
|
|
1437
1760
|
CLIENT_PROOF_CONTENT_TYPE,
|
|
1438
1761
|
CLIENT_PROOF_HEADERS,
|
|
1439
1762
|
CLIENT_PROOF_PROFILE,
|
|
@@ -1454,12 +1777,15 @@ export {
|
|
|
1454
1777
|
PROOF_SIGNATURE_HEX_LENGTH,
|
|
1455
1778
|
ProofInputError,
|
|
1456
1779
|
RedisReplayStore,
|
|
1780
|
+
SERVER_CONTRACT_HEADERS,
|
|
1457
1781
|
TestClock,
|
|
1458
1782
|
admitClientProofRequest,
|
|
1783
|
+
applyServerContractHeaders,
|
|
1459
1784
|
canonicalProofInput,
|
|
1460
1785
|
configureClientProofReplayStore,
|
|
1461
1786
|
createClientProofDevHandler,
|
|
1462
1787
|
createClientProofGuard,
|
|
1788
|
+
createClientVersionMiddleware,
|
|
1463
1789
|
decodeEchoRequest,
|
|
1464
1790
|
decodeHandshakeRequest,
|
|
1465
1791
|
decodeListItemsRequest,
|
|
@@ -1468,13 +1794,18 @@ export {
|
|
|
1468
1794
|
encodeHandshakeResponse,
|
|
1469
1795
|
encodeListItemsResponse,
|
|
1470
1796
|
getClientProofReplayStore,
|
|
1797
|
+
isAppKind,
|
|
1471
1798
|
isCanonicalBytes,
|
|
1799
|
+
isContractVersionSupported,
|
|
1472
1800
|
isRequestContentType,
|
|
1801
|
+
judgeClientIdentity,
|
|
1473
1802
|
newHexId,
|
|
1474
1803
|
parseCanonicalJson,
|
|
1475
1804
|
parseClientProofPublicKey,
|
|
1805
|
+
readClientIdentity,
|
|
1476
1806
|
readCredentials,
|
|
1477
1807
|
replayLedgerKey,
|
|
1808
|
+
serverContractHeaders,
|
|
1478
1809
|
sha256Hex,
|
|
1479
1810
|
signClientProof,
|
|
1480
1811
|
systemClock,
|