@spfn/auth 0.3.0-beta.3 → 0.3.0-beta.4
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 +33 -2
- package/dist/client-proof.d.ts +23 -4
- package/dist/client-proof.js +57 -2
- package/dist/client-proof.js.map +1 -1
- package/dist/server.js +57 -2
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -236,12 +236,13 @@ await authApi.revokeAllKeys.call({ body: {} }); // other devices o
|
|
|
236
236
|
await authApi.revokeAllKeys.call({ body: { includeCurrent: true } }); // everything
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
-
> **All three are POST with their arguments in the body, deliberately.** The mobile auth
|
|
239
|
+
> **All three key-management operations are POST with their arguments in the body, deliberately.** The mobile auth
|
|
240
240
|
> profile (clientProofV1) signs the request body, and `canonical-json` fixes exactly how those
|
|
241
241
|
> bytes are written. A `GET` has no body to sign, and a value in the path has no such rule —
|
|
242
242
|
> client and server could disagree on the signed string over percent-encoding, a trailing
|
|
243
243
|
> slash, or a proxy rewrite alone, and the request would be refused with nothing in the logs
|
|
244
|
-
> naming the cause.
|
|
244
|
+
> naming the cause. Proof-bearing auth operations are shaped this way; the unproven,
|
|
245
|
+
> bodyless `core.time` synchronization prerequisite is the explicit exception.
|
|
245
246
|
|
|
246
247
|
- **The public key never leaves the server**, and the fingerprint is truncated to 8 characters.
|
|
247
248
|
The list exists to recognise a device and point at it; the full fingerprint is what a native
|
|
@@ -917,6 +918,12 @@ the fixed-string contract error envelope (`PROOF_INVALID` · `PROOF_REPLAYED` ·
|
|
|
917
918
|
`SESSION_REVOKED` · `PROFILE_REJECTED` · `CONTRACT_UNSUPPORTED` — SDKs classify by code, never
|
|
918
919
|
HTTP status).
|
|
919
920
|
|
|
921
|
+
Before minting the first proof in each client process, the client calls the built-in
|
|
922
|
+
`GET /_core/time` operation (`core.time`) and establishes its proof epoch from
|
|
923
|
+
`serverTimeMillis`. This prerequisite is unproven and session-free. If the operation is
|
|
924
|
+
unavailable or its response cannot be decoded, proof minting fails closed — there is no silent
|
|
925
|
+
fallback to the device's unsynchronized wall clock.
|
|
926
|
+
|
|
920
927
|
- Wire headers (D23, ratified): `x-spfn-auth-profile`, `x-spfn-client-id`, `x-spfn-key-id`,
|
|
921
928
|
`x-spfn-nonce`, `x-spfn-issued-at`, `x-spfn-proof`, `x-spfn-session`.
|
|
922
929
|
- A request body must be **byte-canonical** — a body that parses but re-encodes differently is
|
|
@@ -944,6 +951,29 @@ HTTP status).
|
|
|
944
951
|
construction or through the `/control/register-key` hook; the private half never reaches
|
|
945
952
|
the server. No persistence — a production enrollment/rotation story is phase 2.
|
|
946
953
|
|
|
954
|
+
### Clock synchronization and proof-time boundaries (contract 0.9.0)
|
|
955
|
+
|
|
956
|
+
`core.time` is imported from `@spfn/core` rather than restated by auth: operation ID, method,
|
|
957
|
+
path, auth class, session requirement, and the closed `ServerTimeResponse` schema all come from
|
|
958
|
+
the core route contract. The mobile contract records it as a bodyless GET prerequisite and
|
|
959
|
+
requires one synchronization before the first proof minted in each process. It does not prescribe
|
|
960
|
+
persistent offset storage, retry sleeps, or device-specific margins.
|
|
961
|
+
|
|
962
|
+
The server admission rule remains strict: `age = serverNow - issuedAtMillis` must satisfy
|
|
963
|
+
`0 <= age <= 300000`. Synchronization does not widen the replay window or change nonce retention.
|
|
964
|
+
A refused request still leaves its nonce unused; only admission spends it.
|
|
965
|
+
|
|
966
|
+
| `serverNow - issuedAtMillis` | Result |
|
|
967
|
+
|---:|---|
|
|
968
|
+
| `0` | accept |
|
|
969
|
+
| `-1` (proof is 1 ms in the future) | `PROOF_EXPIRED` |
|
|
970
|
+
| `300000` | accept |
|
|
971
|
+
| `300001` | `PROOF_EXPIRED` |
|
|
972
|
+
|
|
973
|
+
When `core.time` cannot be read, the client must surface that synchronization failure and stop
|
|
974
|
+
before sending a proof. Using `Date.now()` or a platform wall clock as an implicit fallback would
|
|
975
|
+
reintroduce the skew failure this prerequisite closes.
|
|
976
|
+
|
|
947
977
|
### The contract version on the wire (contract 0.6.0)
|
|
948
978
|
|
|
949
979
|
A client compiled and shipped separately from the server cannot be fixed by redeploying. Until
|
|
@@ -992,6 +1022,7 @@ Every operation in the exported bundle carries `since` — the contract version
|
|
|
992
1022
|
| `auth.clientProof.handshake`, `echo.send`, `items.list` | 0.1.0 |
|
|
993
1023
|
| `auth.enroll.register`, `auth.enroll.login`, `auth.enroll.oauthNative`, `auth.keys.rotate` | 0.3.0 |
|
|
994
1024
|
| `auth.keys.list`, `auth.keys.revoke`, `auth.keys.revokeAll` | 0.4.1 |
|
|
1025
|
+
| `core.time` | 0.9.0 |
|
|
995
1026
|
|
|
996
1027
|
- **This is history, not policy.** The mobile contract's compatibility policy is `allOrNothing`: one
|
|
997
1028
|
contract version passes or refuses the whole surface, so these three fields change no verdict here.
|
package/dist/client-proof.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { KeyObject } from 'node:crypto';
|
|
2
2
|
import { C as ClientProofRefusal } from './wire-version-CtzMKvBB.js';
|
|
3
3
|
export { a as CLIENT_IDENTITY_HEADERS, b as CLIENT_KINDS, c as ClientIdentity, d as ClientKind, e as ClientProofErrorCode, S as SERVER_CONTRACT_HEADERS, f as applyServerContractHeaders, i as isAppKind, g as isContractVersionSupported, j as judgeClientIdentity, n as newHexId, r as readClientIdentity, s as serverContractHeaders } from './wire-version-CtzMKvBB.js';
|
|
4
|
+
import { CORE_TIME_OPERATION_ID } from '@spfn/core/server';
|
|
4
5
|
import { MiddlewareHandler, Context } from 'hono';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -397,8 +398,8 @@ declare function getClientProofReplayStore(): ClientProofReplayStore;
|
|
|
397
398
|
*/
|
|
398
399
|
|
|
399
400
|
interface ContractOperation {
|
|
400
|
-
id: 'auth.clientProof.handshake' | 'echo.send' | 'items.list' | 'auth.enroll.register' | 'auth.enroll.login' | 'auth.enroll.oauthNative' | 'auth.keys.rotate' | 'auth.keys.list' | 'auth.keys.revoke' | 'auth.keys.revokeAll';
|
|
401
|
-
method: 'POST';
|
|
401
|
+
id: 'auth.clientProof.handshake' | 'echo.send' | 'items.list' | 'auth.enroll.register' | 'auth.enroll.login' | 'auth.enroll.oauthNative' | 'auth.keys.rotate' | 'auth.keys.list' | 'auth.keys.revoke' | 'auth.keys.revokeAll' | typeof CORE_TIME_OPERATION_ID;
|
|
402
|
+
method: 'GET' | 'POST';
|
|
402
403
|
path: string;
|
|
403
404
|
/**
|
|
404
405
|
* How a call is admitted. `clientProofV1` operations run the proof
|
|
@@ -408,7 +409,8 @@ interface ContractOperation {
|
|
|
408
409
|
*/
|
|
409
410
|
authProfile: 'clientProofV1' | 'none';
|
|
410
411
|
requiresSession: boolean;
|
|
411
|
-
|
|
412
|
+
/** Absent only when the operation has no request body. */
|
|
413
|
+
requestType?: string;
|
|
412
414
|
responseType: string;
|
|
413
415
|
summary: string;
|
|
414
416
|
/**
|
|
@@ -439,6 +441,23 @@ interface ContractOperation {
|
|
|
439
441
|
*/
|
|
440
442
|
removedIn?: string;
|
|
441
443
|
}
|
|
444
|
+
/** Validated projection of the imported core route contract. */
|
|
445
|
+
declare const IMPORTED_CORE_TIME_CONTRACT: {
|
|
446
|
+
readonly id: "core.time";
|
|
447
|
+
readonly method: "GET";
|
|
448
|
+
readonly path: string;
|
|
449
|
+
readonly authProfile: "none";
|
|
450
|
+
readonly requiresSession: false;
|
|
451
|
+
readonly sourceSince: string;
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* The core capability clientProofV1 needs before the client can mint a proof.
|
|
455
|
+
*
|
|
456
|
+
* Transport and admission fields come from core's exported route contract so
|
|
457
|
+
* auth cannot silently restate a different path or policy. `since` is the
|
|
458
|
+
* mobile-contract history, not core's package-contract history.
|
|
459
|
+
*/
|
|
460
|
+
declare const CORE_PREREQUISITE_OPERATIONS: readonly ContractOperation[];
|
|
442
461
|
declare const CONTRACT_OPERATIONS: readonly ContractOperation[];
|
|
443
462
|
/**
|
|
444
463
|
* The `/_auth` surface exported into the mobile contract: enrollment, login
|
|
@@ -603,4 +622,4 @@ declare const CLIENT_IDENTITY_CONTEXT_KEY = "clientIdentity";
|
|
|
603
622
|
*/
|
|
604
623
|
declare function createClientVersionMiddleware(): MiddlewareHandler;
|
|
605
624
|
|
|
606
|
-
export { ABSENT_BODY_SHA256, AUTH_SURFACE_OPERATIONS, type Admission, CLIENT_IDENTITY_CONTEXT_KEY, 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 ClientProofClock, type ClientProofContext, type ClientProofCredentials, type ClientProofDevHandler, type ClientProofDevHandlerOptions, 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, TestClock, admitClientProofRequest, canonicalProofInput, clientProofRefusalResponse, configureClientProofReplayStore, createClientProofDevHandler, createClientProofGuard, createClientVersionMiddleware, decodeEchoRequest, decodeHandshakeRequest, decodeListItemsRequest, encodeCanonicalJson, encodeEchoResponse, encodeHandshakeResponse, encodeListItemsResponse, getClientProofReplayStore, isCanonicalBytes, isRequestContentType, parseCanonicalJson, parseClientProofPublicKey, readCredentials, replayLedgerKey, sha256Hex, signClientProof, systemClock, verifyClientProof };
|
|
625
|
+
export { ABSENT_BODY_SHA256, AUTH_SURFACE_OPERATIONS, type Admission, CLIENT_IDENTITY_CONTEXT_KEY, CLIENT_PROOF_CONTENT_TYPE, CLIENT_PROOF_HEADERS, CLIENT_PROOF_PROFILE, CONTRACT_OPERATIONS, CONTROL_PREFIX, CONTROL_TOKEN_HEADER, CORE_PREREQUISITE_OPERATIONS, CanonicalJsonError, type CanonicalJsonErrorCode, type CanonicalObject, type CanonicalValue, type ClientProofClock, type ClientProofContext, type ClientProofCredentials, type ClientProofDevHandler, type ClientProofDevHandlerOptions, 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, IMPORTED_CORE_TIME_CONTRACT, type ListItemsRequest, MemoryReplayLedger, MemoryReplayStore, PROOF_SIGNATURE_BYTES, PROOF_SIGNATURE_HEX_LENGTH, ProofInputError, RedisReplayStore, TestClock, admitClientProofRequest, canonicalProofInput, clientProofRefusalResponse, configureClientProofReplayStore, createClientProofDevHandler, createClientProofGuard, createClientVersionMiddleware, decodeEchoRequest, decodeHandshakeRequest, decodeListItemsRequest, encodeCanonicalJson, encodeEchoResponse, encodeHandshakeResponse, encodeListItemsResponse, getClientProofReplayStore, isCanonicalBytes, isRequestContentType, parseCanonicalJson, parseClientProofPublicKey, readCredentials, replayLedgerKey, sha256Hex, signClientProof, systemClock, verifyClientProof };
|
package/dist/client-proof.js
CHANGED
|
@@ -935,6 +935,37 @@ function isRequestContentType(value) {
|
|
|
935
935
|
}
|
|
936
936
|
|
|
937
937
|
// src/server/client-proof/contract-types.ts
|
|
938
|
+
import {
|
|
939
|
+
CORE_TIME_OPERATION_ID,
|
|
940
|
+
CORE_TIME_ROUTE
|
|
941
|
+
} from "@spfn/core/server";
|
|
942
|
+
function importCoreTimeContract() {
|
|
943
|
+
const { method, path, contract } = CORE_TIME_ROUTE;
|
|
944
|
+
if (method !== "GET" || typeof path !== "string" || contract?.auth !== "none" || contract.requiresSession !== false || typeof contract.since !== "string") {
|
|
945
|
+
throw new Error("core.time does not match the clientProofV1 synchronization prerequisite");
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
id: CORE_TIME_OPERATION_ID,
|
|
949
|
+
method,
|
|
950
|
+
path,
|
|
951
|
+
authProfile: contract.auth,
|
|
952
|
+
requiresSession: contract.requiresSession,
|
|
953
|
+
sourceSince: contract.since
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
var IMPORTED_CORE_TIME_CONTRACT = importCoreTimeContract();
|
|
957
|
+
var CORE_PREREQUISITE_OPERATIONS = [
|
|
958
|
+
{
|
|
959
|
+
id: IMPORTED_CORE_TIME_CONTRACT.id,
|
|
960
|
+
method: IMPORTED_CORE_TIME_CONTRACT.method,
|
|
961
|
+
path: IMPORTED_CORE_TIME_CONTRACT.path,
|
|
962
|
+
authProfile: IMPORTED_CORE_TIME_CONTRACT.authProfile,
|
|
963
|
+
requiresSession: IMPORTED_CORE_TIME_CONTRACT.requiresSession,
|
|
964
|
+
responseType: "ServerTimeResponse",
|
|
965
|
+
summary: "Returns the server epoch used to timestamp clientProofV1 proofs.",
|
|
966
|
+
since: "0.9.0"
|
|
967
|
+
}
|
|
968
|
+
];
|
|
938
969
|
var CONTRACT_OPERATIONS = [
|
|
939
970
|
{
|
|
940
971
|
id: "auth.clientProof.handshake",
|
|
@@ -1287,6 +1318,10 @@ function answer(status, value) {
|
|
|
1287
1318
|
|
|
1288
1319
|
// src/server/client-proof/contract-bundle.ts
|
|
1289
1320
|
import { createHash as createHash2 } from "crypto";
|
|
1321
|
+
import {
|
|
1322
|
+
CORE_TIME_OPERATION_ID as CORE_TIME_OPERATION_ID2,
|
|
1323
|
+
ServerTimeResponseSchema
|
|
1324
|
+
} from "@spfn/core/server";
|
|
1290
1325
|
|
|
1291
1326
|
// src/server/types.ts
|
|
1292
1327
|
var KEY_ALGORITHM = ["ES256", "RS256"];
|
|
@@ -1307,16 +1342,34 @@ function isAppKind(kind) {
|
|
|
1307
1342
|
}
|
|
1308
1343
|
|
|
1309
1344
|
// src/server/client-proof/contract-bundle.ts
|
|
1310
|
-
var CONTRACT_VERSION = "0.
|
|
1345
|
+
var CONTRACT_VERSION = "0.9.0";
|
|
1311
1346
|
var CONTRACT_MAJOR = 0;
|
|
1312
|
-
var CONTRACT_SUPPORTED_RANGE = ">=0.
|
|
1347
|
+
var CONTRACT_SUPPORTED_RANGE = ">=0.9.0 <0.10.0";
|
|
1313
1348
|
function required(name, type) {
|
|
1314
1349
|
return { name, type, optional: false };
|
|
1315
1350
|
}
|
|
1316
1351
|
function optional(name, type) {
|
|
1317
1352
|
return { name, type, optional: true };
|
|
1318
1353
|
}
|
|
1354
|
+
function coreTimeResponseDeclaration() {
|
|
1355
|
+
if (ServerTimeResponseSchema.type !== "object" || ServerTimeResponseSchema.additionalProperties !== false) {
|
|
1356
|
+
throw new Error("core.time response must remain a closed object");
|
|
1357
|
+
}
|
|
1358
|
+
const requiredFields = new Set(ServerTimeResponseSchema.required);
|
|
1359
|
+
const fields = Object.entries(ServerTimeResponseSchema.properties).map(([name, schema]) => {
|
|
1360
|
+
if (schema.type !== "integer") {
|
|
1361
|
+
throw new Error(`core.time response field ${name} is outside the mobile type grammar`);
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
name,
|
|
1365
|
+
type: "integer",
|
|
1366
|
+
optional: !requiredFields.has(name)
|
|
1367
|
+
};
|
|
1368
|
+
});
|
|
1369
|
+
return { name: "ServerTimeResponse", fields };
|
|
1370
|
+
}
|
|
1319
1371
|
var CONTRACT_TYPES = [
|
|
1372
|
+
coreTimeResponseDeclaration(),
|
|
1320
1373
|
{
|
|
1321
1374
|
name: "HandshakeRequest",
|
|
1322
1375
|
fields: [
|
|
@@ -1790,6 +1843,7 @@ export {
|
|
|
1790
1843
|
CONTRACT_OPERATIONS,
|
|
1791
1844
|
CONTROL_PREFIX,
|
|
1792
1845
|
CONTROL_TOKEN_HEADER,
|
|
1846
|
+
CORE_PREREQUISITE_OPERATIONS,
|
|
1793
1847
|
CanonicalJsonError,
|
|
1794
1848
|
ClientProofRefusal,
|
|
1795
1849
|
ClientProofState,
|
|
@@ -1798,6 +1852,7 @@ export {
|
|
|
1798
1852
|
DEFAULT_SESSION_TTL_MILLIS,
|
|
1799
1853
|
DEV_CATALOGUE,
|
|
1800
1854
|
DEV_MAX_LIMIT,
|
|
1855
|
+
IMPORTED_CORE_TIME_CONTRACT,
|
|
1801
1856
|
MemoryReplayLedger,
|
|
1802
1857
|
MemoryReplayStore,
|
|
1803
1858
|
PROOF_SIGNATURE_BYTES,
|