@spfn/auth 0.2.0-beta.90 → 0.2.0-beta.91

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/server.js CHANGED
@@ -1735,8 +1735,8 @@ function RequiredKeys(properties) {
1735
1735
  return keys;
1736
1736
  }
1737
1737
  function _Object(properties, options) {
1738
- const required = RequiredKeys(properties);
1739
- const schematic = required.length > 0 ? { [Kind]: "Object", type: "object", properties, required } : { [Kind]: "Object", type: "object", properties };
1738
+ const required2 = RequiredKeys(properties);
1739
+ const schematic = required2.length > 0 ? { [Kind]: "Object", type: "object", properties, required: required2 } : { [Kind]: "Object", type: "object", properties };
1740
1740
  return CreateType(schematic, options);
1741
1741
  }
1742
1742
  var Object2;
@@ -4725,6 +4725,19 @@ var init_user_profiles = __esm({
4725
4725
  }
4726
4726
  });
4727
4727
 
4728
+ // src/server/client-proof/wire-headers.ts
4729
+ var SERVER_CONTRACT_HEADERS, CLIENT_KINDS;
4730
+ var init_wire_headers = __esm({
4731
+ "src/server/client-proof/wire-headers.ts"() {
4732
+ "use strict";
4733
+ SERVER_CONTRACT_HEADERS = {
4734
+ version: "x-spfn-server-contract-version",
4735
+ supportedRange: "x-spfn-supported-contract-range"
4736
+ };
4737
+ CLIENT_KINDS = ["web", "ios", "android"];
4738
+ }
4739
+ });
4740
+
4728
4741
  // src/server/entities/user-public-keys.ts
4729
4742
  import { text as text4, boolean as boolean3, index as index4 } from "drizzle-orm/pg-core";
4730
4743
  import { id as id4, foreignKey as foreignKey3, enumText as enumText2, utcTimestamp as utcTimestamp2 } from "@spfn/core/db";
@@ -4733,6 +4746,7 @@ var init_user_public_keys = __esm({
4733
4746
  "src/server/entities/user-public-keys.ts"() {
4734
4747
  "use strict";
4735
4748
  init_types();
4749
+ init_wire_headers();
4736
4750
  init_users();
4737
4751
  init_schema4();
4738
4752
  userPublicKeys = authSchema.table(
@@ -4771,6 +4785,26 @@ var init_user_public_keys = __esm({
4771
4785
  // null: the client sent none
4772
4786
  // Used for: the same list, alongside deviceName
4773
4787
  platform: enumText2("platform", KEY_PLATFORM),
4788
+ // What the client said about itself on the last request signed by this key.
4789
+ //
4790
+ // The three come from x-spfn-client-kind, x-spfn-client-version and
4791
+ // x-spfn-client-contract-version. They are client-supplied and
4792
+ // unauthenticated, exactly like deviceName above: nothing is authorized by
4793
+ // them, and a client that lies about its version gains nothing but a wrong
4794
+ // entry in its owner's own device list.
4795
+ //
4796
+ // They exist so the server knows which release each deployed client runs.
4797
+ // Refusing an outdated client is the last resort; reaching its owner first
4798
+ // needs a list of who runs what, and announcing a version is not the same
4799
+ // as the server having recorded it.
4800
+ clientKind: enumText2("client_kind", CLIENT_KINDS),
4801
+ clientVersion: text4("client_version"),
4802
+ clientContractVersion: text4("client_contract_version"),
4803
+ // When any of the three above last changed — an app update, in practice.
4804
+ // Not when they were last seen: a value that moves on every request is a
4805
+ // write on every request, and the question this answers is "since when has
4806
+ // this device been on this release", which only a change can answer.
4807
+ clientSeenAt: utcTimestamp2("client_seen_at"),
4774
4808
  // Key status
4775
4809
  // false: Key is deactivated (cannot be used for verification)
4776
4810
  // Used for: soft key rotation, temporary key suspension
@@ -5697,7 +5731,7 @@ var init_users_repository = __esm({
5697
5731
 
5698
5732
  // src/server/repositories/keys.repository.ts
5699
5733
  import { BaseRepository as BaseRepository2 } from "@spfn/core/db";
5700
- import { eq as eq2, and as and2, or, isNull, lt, ne, desc } from "drizzle-orm";
5734
+ import { eq as eq2, and as and2, or, isNull, lt, ne, desc, sql as sql3 } from "drizzle-orm";
5701
5735
  var LAST_USED_THROTTLE_MS, KeysRepository, keysRepository;
5702
5736
  var init_keys_repository = __esm({
5703
5737
  "src/server/repositories/keys.repository.ts"() {
@@ -5916,17 +5950,44 @@ var init_keys_repository = __esm({
5916
5950
  * LAST_USED_THROTTLE_MS), so a busy key isn't UPDATEd on every request. The
5917
5951
  * throttle lives in the WHERE clause — atomic, no read-then-write race. No
5918
5952
  * RETURNING (callers fire-and-forget and discard the row).
5953
+ *
5954
+ * `identity` is what the client said about itself on this request. It is
5955
+ * recorded on the same row, and the throttle does not apply to it: a version
5956
+ * that changed is written immediately, because an app update is the event this
5957
+ * column exists to catch and waiting a minute to notice it serves nobody. A
5958
+ * version that did not change writes nothing extra — the UPDATE the throttle
5959
+ * was already going to do carries it.
5960
+ *
5961
+ * `clientSeenAt` moves only when one of the three values differs from what is
5962
+ * stored, so it answers "since when has this device been on this release"
5963
+ * rather than "when was it last seen", which lastUsedAt already answers.
5919
5964
  */
5920
- async updateLastUsedById(id12) {
5965
+ async updateLastUsedById(id12, identity) {
5921
5966
  const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
5967
+ const lastUsedIsStale = or(
5968
+ isNull(userPublicKeys.lastUsedAt),
5969
+ lt(userPublicKeys.lastUsedAt, staleBefore)
5970
+ );
5971
+ if (!identity) {
5972
+ await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id12), lastUsedIsStale));
5973
+ return;
5974
+ }
5975
+ const identityChanged = sql3`(
5976
+ ${userPublicKeys.clientKind} IS DISTINCT FROM ${identity.kind}
5977
+ OR ${userPublicKeys.clientVersion} IS DISTINCT FROM ${identity.version}
5978
+ OR ${userPublicKeys.clientContractVersion} IS DISTINCT FROM ${identity.contractVersion}
5979
+ )`;
5980
+ const now = /* @__PURE__ */ new Date();
5981
+ const nowParam = sql3`${now.toISOString()}::timestamptz`;
5922
5982
  await this.db.update(userPublicKeys).set({
5923
- lastUsedAt: /* @__PURE__ */ new Date()
5983
+ lastUsedAt: now,
5984
+ clientKind: identity.kind,
5985
+ clientVersion: identity.version,
5986
+ clientContractVersion: identity.contractVersion,
5987
+ clientSeenAt: sql3`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
5924
5988
  }).where(and2(
5925
5989
  eq2(userPublicKeys.id, id12),
5926
- or(
5927
- isNull(userPublicKeys.lastUsedAt),
5928
- lt(userPublicKeys.lastUsedAt, staleBefore)
5929
- )
5990
+ or(lastUsedIsStale, identityChanged)
5930
5991
  ));
5931
5992
  }
5932
5993
  };
@@ -6511,7 +6572,7 @@ var init_user_profiles_repository = __esm({
6511
6572
  });
6512
6573
 
6513
6574
  // src/server/repositories/invitations.repository.ts
6514
- import { eq as eq9, and as and6, lt as lt4, desc as desc2, sql as sql3 } from "drizzle-orm";
6575
+ import { eq as eq9, and as and6, lt as lt4, desc as desc2, sql as sql4 } from "drizzle-orm";
6515
6576
  import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
6516
6577
  var InvitationsRepository, invitationsRepository;
6517
6578
  var init_invitations_repository = __esm({
@@ -6653,7 +6714,7 @@ var init_invitations_repository = __esm({
6653
6714
  conditions.push(eq9(userInvitations.invitedBy, invitedBy));
6654
6715
  }
6655
6716
  const whereClause = conditions.length > 0 ? and6(...conditions) : void 0;
6656
- const countResult = await this.readDb.select({ count: sql3`count(*)` }).from(userInvitations).where(whereClause);
6717
+ const countResult = await this.readDb.select({ count: sql4`count(*)` }).from(userInvitations).where(whereClause);
6657
6718
  const total = Number(countResult[0]?.count || 0);
6658
6719
  const results = await this.readDb.select({
6659
6720
  id: userInvitations.id,
@@ -11094,21 +11155,8 @@ import {
11094
11155
  KeyExpiredError
11095
11156
  } from "@spfn/auth/errors";
11096
11157
 
11097
- // src/server/middleware/auth-profiles.ts
11098
- import {
11099
- BadRequestError as BadRequestError2,
11100
- ConflictError as ConflictError2,
11101
- ServiceUnavailableError,
11102
- UnauthorizedError
11103
- } from "@spfn/core/errors";
11104
- import {
11105
- authLogger as authLogger2,
11106
- keysRepository as keysRepository2,
11107
- usersRepository as usersRepository2,
11108
- userProfilesRepository as userProfilesRepository2,
11109
- getPendingDeletionInfo as getPendingDeletionInfo2
11110
- } from "@spfn/auth/server";
11111
- import { AccountDisabledError as AccountDisabledError3, AccountPendingDeletionError as AccountPendingDeletionError3 } from "@spfn/auth/errors";
11158
+ // src/server/client-proof/refusal.ts
11159
+ import { randomBytes } from "crypto";
11112
11160
 
11113
11161
  // src/server/client-proof/canonical-json.ts
11114
11162
  var CanonicalJsonError = class extends Error {
@@ -11456,82 +11504,7 @@ function encodeString(value) {
11456
11504
  return out + '"';
11457
11505
  }
11458
11506
 
11459
- // src/server/client-proof/proof.ts
11460
- import { createHash as createHash5, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
11461
- var CLIENT_PROOF_PROFILE = "clientProofV1";
11462
- var ABSENT_BODY_SHA256 = "0".repeat(64);
11463
- var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
11464
- var PROOF_INPUT_FIELDS = [
11465
- "profile",
11466
- "method",
11467
- "path",
11468
- "clientId",
11469
- "keyId",
11470
- "nonce",
11471
- "issuedAtMillis",
11472
- "bodySha256"
11473
- ];
11474
- var PROOF_INPUT_SEPARATOR = "\n";
11475
- var PROOF_SIGNATURE_BYTES = 64;
11476
- var PROOF_SIGNATURE_HEX_LENGTH = PROOF_SIGNATURE_BYTES * 2;
11477
- var PROOF_SIGNATURE_PATTERN = /^[0-9a-f]{128}$/;
11478
- var RAW_SIGNATURE_ENCODING = "ieee-p1363";
11479
- var ProofInputError = class extends Error {
11480
- constructor() {
11481
- super("proof input field contains a C0 control character");
11482
- this.name = "ProofInputError";
11483
- }
11484
- };
11485
- function canonicalProofInput(input) {
11486
- const values = {
11487
- profile: CLIENT_PROOF_PROFILE,
11488
- method: input.method,
11489
- path: input.path,
11490
- clientId: input.clientId,
11491
- keyId: input.keyId,
11492
- nonce: input.nonce,
11493
- issuedAtMillis: input.issuedAtMillis.toString(),
11494
- bodySha256: input.bodySha256
11495
- };
11496
- const fields = PROOF_INPUT_FIELDS.map((name) => values[name]);
11497
- for (const field of fields) {
11498
- for (const ch of field) {
11499
- if (ch.codePointAt(0) < 32) {
11500
- throw new ProofInputError();
11501
- }
11502
- }
11503
- }
11504
- return fields.join(PROOF_INPUT_SEPARATOR);
11505
- }
11506
- function parseClientProofPublicKey(spkiDerBase64) {
11507
- const key = createPublicKey({
11508
- key: Buffer.from(spkiDerBase64, "base64"),
11509
- format: "der",
11510
- type: "spki"
11511
- });
11512
- if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
11513
- throw new Error("a clientProofV1 public key must be an ECDSA P-256 key");
11514
- }
11515
- return key;
11516
- }
11517
- function verifyClientProof(input, presentedProof, publicKey) {
11518
- const data = Buffer.from(canonicalProofInput(input), "utf8");
11519
- if (!PROOF_SIGNATURE_PATTERN.test(presentedProof)) {
11520
- return false;
11521
- }
11522
- return verify2(
11523
- "sha256",
11524
- data,
11525
- { key: publicKey, dsaEncoding: RAW_SIGNATURE_ENCODING },
11526
- Buffer.from(presentedProof, "hex")
11527
- );
11528
- }
11529
- function sha256Hex(bytes) {
11530
- return createHash5("sha256").update(bytes).digest("hex");
11531
- }
11532
-
11533
11507
  // src/server/client-proof/refusal.ts
11534
- import { randomBytes } from "crypto";
11535
11508
  var HTTP_STATUS = {
11536
11509
  PROOF_INVALID: 401,
11537
11510
  PROOF_REPLAYED: 401,
@@ -11540,6 +11513,9 @@ var HTTP_STATUS = {
11540
11513
  PROFILE_REJECTED: 400,
11541
11514
  CONTRACT_UNSUPPORTED: 409
11542
11515
  };
11516
+ function newHexId() {
11517
+ return randomBytes(16).toString("hex");
11518
+ }
11543
11519
  var ClientProofRefusal = class _ClientProofRefusal {
11544
11520
  constructor(code, message) {
11545
11521
  this.code = code;
@@ -11609,6 +11585,18 @@ var ClientProofRefusal = class _ClientProofRefusal {
11609
11585
  static profileRejected() {
11610
11586
  return new _ClientProofRefusal("PROFILE_REJECTED", "the named auth profile is not on this contract's allowlist");
11611
11587
  }
11588
+ /**
11589
+ * A request that names a profile and presents Bearer credentials as well.
11590
+ * The profile named is a real one, so this is not a shape the two ends
11591
+ * disagree about: the request asked to be authenticated two ways at once
11592
+ * and the profile it named is the one refused.
11593
+ */
11594
+ static credentialsMixed() {
11595
+ return new _ClientProofRefusal(
11596
+ "PROFILE_REJECTED",
11597
+ "an auth profile and Bearer credentials must not be mixed in one request"
11598
+ );
11599
+ }
11612
11600
  // ---- auth: a new session might clear it (rule 1) -------------------------
11613
11601
  static sessionRevoked() {
11614
11602
  return new _ClientProofRefusal("SESSION_REVOKED", "the key or session was revoked");
@@ -11627,6 +11615,84 @@ function contractViolation(message) {
11627
11615
  return new ClientProofRefusal("CONTRACT_UNSUPPORTED", message);
11628
11616
  }
11629
11617
 
11618
+ // src/server/client-proof/contract-bundle.ts
11619
+ import { createHash as createHash6 } from "crypto";
11620
+ init_types();
11621
+
11622
+ // src/server/client-proof/proof.ts
11623
+ import { createHash as createHash5, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
11624
+ var CLIENT_PROOF_PROFILE = "clientProofV1";
11625
+ var ABSENT_BODY_SHA256 = "0".repeat(64);
11626
+ var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
11627
+ var PROOF_INPUT_FIELDS = [
11628
+ "profile",
11629
+ "method",
11630
+ "path",
11631
+ "clientId",
11632
+ "keyId",
11633
+ "nonce",
11634
+ "issuedAtMillis",
11635
+ "bodySha256"
11636
+ ];
11637
+ var PROOF_INPUT_SEPARATOR = "\n";
11638
+ var PROOF_SIGNATURE_BYTES = 64;
11639
+ var PROOF_SIGNATURE_HEX_LENGTH = PROOF_SIGNATURE_BYTES * 2;
11640
+ var PROOF_SIGNATURE_PATTERN = /^[0-9a-f]{128}$/;
11641
+ var RAW_SIGNATURE_ENCODING = "ieee-p1363";
11642
+ var ProofInputError = class extends Error {
11643
+ constructor() {
11644
+ super("proof input field contains a C0 control character");
11645
+ this.name = "ProofInputError";
11646
+ }
11647
+ };
11648
+ function canonicalProofInput(input) {
11649
+ const values = {
11650
+ profile: CLIENT_PROOF_PROFILE,
11651
+ method: input.method,
11652
+ path: input.path,
11653
+ clientId: input.clientId,
11654
+ keyId: input.keyId,
11655
+ nonce: input.nonce,
11656
+ issuedAtMillis: input.issuedAtMillis.toString(),
11657
+ bodySha256: input.bodySha256
11658
+ };
11659
+ const fields = PROOF_INPUT_FIELDS.map((name) => values[name]);
11660
+ for (const field of fields) {
11661
+ for (const ch of field) {
11662
+ if (ch.codePointAt(0) < 32) {
11663
+ throw new ProofInputError();
11664
+ }
11665
+ }
11666
+ }
11667
+ return fields.join(PROOF_INPUT_SEPARATOR);
11668
+ }
11669
+ function parseClientProofPublicKey(spkiDerBase64) {
11670
+ const key = createPublicKey({
11671
+ key: Buffer.from(spkiDerBase64, "base64"),
11672
+ format: "der",
11673
+ type: "spki"
11674
+ });
11675
+ if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
11676
+ throw new Error("a clientProofV1 public key must be an ECDSA P-256 key");
11677
+ }
11678
+ return key;
11679
+ }
11680
+ function verifyClientProof(input, presentedProof, publicKey) {
11681
+ const data = Buffer.from(canonicalProofInput(input), "utf8");
11682
+ if (!PROOF_SIGNATURE_PATTERN.test(presentedProof)) {
11683
+ return false;
11684
+ }
11685
+ return verify2(
11686
+ "sha256",
11687
+ data,
11688
+ { key: publicKey, dsaEncoding: RAW_SIGNATURE_ENCODING },
11689
+ Buffer.from(presentedProof, "hex")
11690
+ );
11691
+ }
11692
+ function sha256Hex(bytes) {
11693
+ return createHash5("sha256").update(bytes).digest("hex");
11694
+ }
11695
+
11630
11696
  // src/server/client-proof/admission.ts
11631
11697
  var CLIENT_PROOF_HEADERS = {
11632
11698
  profile: "x-spfn-auth-profile",
@@ -11681,6 +11747,253 @@ function isRequestContentType(value) {
11681
11747
  return value.split(";")[0].trim().toLowerCase() === CLIENT_PROOF_CONTENT_TYPE;
11682
11748
  }
11683
11749
 
11750
+ // src/server/client-proof/contract-bundle.ts
11751
+ init_wire_headers();
11752
+ var CONTRACT_VERSION = "0.8.0";
11753
+ var CONTRACT_SUPPORTED_RANGE = ">=0.8.0 <0.9.0";
11754
+ function required(name, type) {
11755
+ return { name, type, optional: false };
11756
+ }
11757
+ function optional(name, type) {
11758
+ return { name, type, optional: true };
11759
+ }
11760
+ var CONTRACT_TYPES = [
11761
+ {
11762
+ name: "HandshakeRequest",
11763
+ fields: [
11764
+ required("clientId", "string"),
11765
+ required("keyId", "string"),
11766
+ required("nonce", "string"),
11767
+ required("issuedAtMillis", "integer")
11768
+ ]
11769
+ },
11770
+ {
11771
+ name: "HandshakeResponse",
11772
+ fields: [
11773
+ required("sessionId", "string"),
11774
+ required("expiresAtMillis", "integer")
11775
+ ]
11776
+ },
11777
+ {
11778
+ name: "EchoRequest",
11779
+ fields: [
11780
+ required("message", "string"),
11781
+ required("sequence", "integer")
11782
+ ]
11783
+ },
11784
+ {
11785
+ name: "EchoResponse",
11786
+ fields: [
11787
+ required("message", "string"),
11788
+ required("sequence", "integer"),
11789
+ required("serverTimeMillis", "integer")
11790
+ ]
11791
+ },
11792
+ {
11793
+ name: "ListItemsRequest",
11794
+ fields: [
11795
+ required("limit", "integer"),
11796
+ optional("cursor", "string")
11797
+ ]
11798
+ },
11799
+ {
11800
+ name: "Item",
11801
+ fields: [
11802
+ required("id", "string"),
11803
+ required("name", "string"),
11804
+ required("updatedAtMillis", "integer")
11805
+ ]
11806
+ },
11807
+ {
11808
+ name: "ListItemsResponse",
11809
+ fields: [
11810
+ required("items", "array<Item>"),
11811
+ optional("nextCursor", "string")
11812
+ ]
11813
+ },
11814
+ {
11815
+ name: "RegisterRequest",
11816
+ fields: [
11817
+ optional("email", "string"),
11818
+ optional("phone", "string"),
11819
+ required("verificationToken", "string"),
11820
+ required("password", "string"),
11821
+ required("publicKey", "string"),
11822
+ required("keyId", "string"),
11823
+ required("fingerprint", "string"),
11824
+ required("algorithm", "KeyAlgorithm")
11825
+ ]
11826
+ },
11827
+ {
11828
+ name: "RegisterResponse",
11829
+ fields: [
11830
+ required("userId", "string"),
11831
+ required("publicId", "string"),
11832
+ optional("email", "string"),
11833
+ optional("phone", "string")
11834
+ ]
11835
+ },
11836
+ {
11837
+ name: "LoginRequest",
11838
+ fields: [
11839
+ optional("email", "string"),
11840
+ optional("phone", "string"),
11841
+ required("password", "string"),
11842
+ required("publicKey", "string"),
11843
+ required("keyId", "string"),
11844
+ required("fingerprint", "string"),
11845
+ required("algorithm", "KeyAlgorithm"),
11846
+ optional("oldKeyId", "string")
11847
+ ]
11848
+ },
11849
+ {
11850
+ name: "LoginResponse",
11851
+ fields: [
11852
+ required("userId", "string"),
11853
+ required("publicId", "string"),
11854
+ optional("email", "string"),
11855
+ optional("phone", "string"),
11856
+ required("passwordChangeRequired", "boolean")
11857
+ ]
11858
+ },
11859
+ {
11860
+ name: "OauthNativeRequest",
11861
+ fields: [
11862
+ required("idToken", "string"),
11863
+ required("nonce", "string"),
11864
+ optional("accessToken", "string"),
11865
+ required("publicKey", "string"),
11866
+ required("keyId", "string"),
11867
+ required("fingerprint", "string"),
11868
+ required("algorithm", "KeyAlgorithm")
11869
+ ]
11870
+ },
11871
+ {
11872
+ name: "OauthNativeResponse",
11873
+ fields: [
11874
+ required("userId", "string"),
11875
+ required("keyId", "string"),
11876
+ required("isNewUser", "boolean")
11877
+ ]
11878
+ },
11879
+ {
11880
+ name: "RotateKeyRequest",
11881
+ fields: [
11882
+ required("publicKey", "string"),
11883
+ required("keyId", "string"),
11884
+ required("fingerprint", "string"),
11885
+ required("algorithm", "KeyAlgorithm")
11886
+ ]
11887
+ },
11888
+ {
11889
+ name: "RotateKeyResponse",
11890
+ fields: [
11891
+ required("success", "boolean"),
11892
+ required("keyId", "string")
11893
+ ]
11894
+ },
11895
+ {
11896
+ name: "ListKeysRequest",
11897
+ fields: [
11898
+ optional("includeRevoked", "boolean")
11899
+ ]
11900
+ },
11901
+ {
11902
+ name: "KeySummary",
11903
+ fields: [
11904
+ required("keyId", "string"),
11905
+ optional("deviceName", "string"),
11906
+ optional("platform", "string"),
11907
+ required("algorithm", "KeyAlgorithm"),
11908
+ required("fingerprintPrefix", "string"),
11909
+ required("createdAtMillis", "integer"),
11910
+ optional("lastUsedAtMillis", "integer"),
11911
+ optional("expiresAtMillis", "integer"),
11912
+ required("isExpired", "boolean"),
11913
+ required("isActive", "boolean"),
11914
+ optional("revokedAtMillis", "integer")
11915
+ ]
11916
+ },
11917
+ {
11918
+ name: "ListKeysResponse",
11919
+ fields: [
11920
+ required("keys", "array<KeySummary>")
11921
+ ]
11922
+ },
11923
+ {
11924
+ name: "RevokeKeyRequest",
11925
+ fields: [
11926
+ required("keyId", "string")
11927
+ ]
11928
+ },
11929
+ {
11930
+ name: "RevokeKeyResponse",
11931
+ fields: [
11932
+ required("keyId", "string"),
11933
+ required("selfRevoked", "boolean")
11934
+ ]
11935
+ },
11936
+ {
11937
+ name: "RevokeAllKeysRequest",
11938
+ fields: [
11939
+ optional("includeCurrent", "boolean")
11940
+ ]
11941
+ },
11942
+ {
11943
+ name: "RevokeAllKeysResponse",
11944
+ fields: [
11945
+ required("revokedCount", "integer"),
11946
+ required("currentKeyRevoked", "boolean")
11947
+ ]
11948
+ }
11949
+ ];
11950
+ var CONTRACT_ENUMS = [
11951
+ { name: "KeyAlgorithm", values: [...KEY_ALGORITHM] }
11952
+ ];
11953
+ var BUNDLE_FILENAME = "spfn-mobile-contract.json";
11954
+ var BUNDLE_REPO_PATH = `contracts/mobile/${BUNDLE_FILENAME}`;
11955
+
11956
+ // src/server/client-proof/wire-version.ts
11957
+ init_wire_headers();
11958
+ init_wire_headers();
11959
+ function serverContractHeaders() {
11960
+ return {
11961
+ [SERVER_CONTRACT_HEADERS.version]: CONTRACT_VERSION,
11962
+ [SERVER_CONTRACT_HEADERS.supportedRange]: CONTRACT_SUPPORTED_RANGE
11963
+ };
11964
+ }
11965
+
11966
+ // src/server/client-proof/version-middleware.ts
11967
+ var CLIENT_IDENTITY_CONTEXT_KEY = "clientIdentity";
11968
+ function readContextClientIdentity(c) {
11969
+ return c.get(CLIENT_IDENTITY_CONTEXT_KEY) ?? null;
11970
+ }
11971
+
11972
+ // src/server/middleware/auth-profiles.ts
11973
+ import {
11974
+ SerializableError,
11975
+ ServiceUnavailableError,
11976
+ UnauthorizedError
11977
+ } from "@spfn/core/errors";
11978
+ import {
11979
+ authLogger as authLogger2,
11980
+ keysRepository as keysRepository2,
11981
+ usersRepository as usersRepository2,
11982
+ userProfilesRepository as userProfilesRepository2,
11983
+ getPendingDeletionInfo as getPendingDeletionInfo2
11984
+ } from "@spfn/auth/server";
11985
+ import { AccountDisabledError as AccountDisabledError3, AccountPendingDeletionError as AccountPendingDeletionError3 } from "@spfn/auth/errors";
11986
+
11987
+ // src/server/client-proof/refusal-response.ts
11988
+ function clientProofRefusalResponse(c, refusal) {
11989
+ const bytes = refusal.envelopeBytes(newHexId());
11990
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
11991
+ return c.newResponse(buffer, refusal.httpStatus, {
11992
+ "content-type": "application/json",
11993
+ ...serverContractHeaders()
11994
+ });
11995
+ }
11996
+
11684
11997
  // src/server/client-proof/replay-store.ts
11685
11998
  import { getCache } from "@spfn/core/cache";
11686
11999
  function replayLedgerKey(clientId, nonce) {
@@ -11744,10 +12057,7 @@ function selectAuthProfile(c) {
11744
12057
  return null;
11745
12058
  }
11746
12059
  if (c.req.header("Authorization") !== void 0) {
11747
- throw new BadRequestError2({
11748
- message: "an auth profile and Bearer credentials must not be mixed in one request",
11749
- details: { code: "PROFILE_REJECTED" }
11750
- });
12060
+ throw refusalError(ClientProofRefusal.credentialsMixed());
11751
12061
  }
11752
12062
  const verifier = AUTH_PROFILE_VERIFIERS.get(profile);
11753
12063
  if (verifier === void 0) {
@@ -11773,15 +12083,34 @@ async function resolveAuthenticatedUser(userId) {
11773
12083
  }
11774
12084
  return { user, role: role?.name ?? null, locale };
11775
12085
  }
11776
- function refusalError(refusal) {
11777
- const data = { message: refusal.message, details: { code: refusal.code } };
11778
- if (refusal.code === "PROFILE_REJECTED") {
11779
- return new BadRequestError2(data);
12086
+ var ClientProofRefusalError = class extends SerializableError {
12087
+ constructor(refusal) {
12088
+ super(refusal.message);
12089
+ this.refusal = refusal;
12090
+ this.name = "ClientProofRefusalError";
12091
+ this.statusCode = refusal.httpStatus;
11780
12092
  }
11781
- if (refusal.code === "CONTRACT_UNSUPPORTED") {
11782
- return new ConflictError2(data);
12093
+ statusCode;
12094
+ toJSON() {
12095
+ return { __type: this.refusal.code, message: this.message };
12096
+ }
12097
+ };
12098
+ function refusalError(refusal) {
12099
+ return new ClientProofRefusalError(refusal);
12100
+ }
12101
+ async function runAuthProfile(c) {
12102
+ try {
12103
+ const verifier = selectAuthProfile(c);
12104
+ if (verifier === null) {
12105
+ return { kind: "none" };
12106
+ }
12107
+ return { kind: "authenticated", auth: await verifier.verify(c) };
12108
+ } catch (err) {
12109
+ if (err instanceof ClientProofRefusalError) {
12110
+ return { kind: "refused", response: clientProofRefusalResponse(c, err.refusal) };
12111
+ }
12112
+ throw err;
11783
12113
  }
11784
- return new UnauthorizedError(data);
11785
12114
  }
11786
12115
  async function failClosed(operation) {
11787
12116
  try {
@@ -11841,7 +12170,7 @@ async function verifyClientProofProfile(c) {
11841
12170
  throw refusalError(ClientProofRefusal.proofReplayed());
11842
12171
  }
11843
12172
  const { user, role, locale } = await resolveAuthenticatedUser(keyRecord.userId);
11844
- keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
12173
+ keysRepository2.updateLastUsedById(keyRecord.id, readContextClientIdentity(c)).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
11845
12174
  authLogger2.middleware.info("API access", {
11846
12175
  userId: user.id,
11847
12176
  email: user.email,
@@ -11900,11 +12229,14 @@ var AUTH_PROFILE_VERIFIERS = /* @__PURE__ */ new Map([
11900
12229
 
11901
12230
  // src/server/middleware/authenticate.ts
11902
12231
  var authenticate = defineMiddleware("auth", async (c, next) => {
11903
- const profileVerifier = selectAuthProfile(c);
11904
- if (profileVerifier !== null) {
11905
- c.set("auth", await profileVerifier.verify(c));
12232
+ const profile = await runAuthProfile(c);
12233
+ if (profile.kind === "refused") {
12234
+ return profile.response;
12235
+ }
12236
+ if (profile.kind === "authenticated") {
12237
+ c.set("auth", profile.auth);
11906
12238
  await next();
11907
- return;
12239
+ return void 0;
11908
12240
  }
11909
12241
  const authHeader = c.req.header("Authorization");
11910
12242
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
@@ -11946,7 +12278,7 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
11946
12278
  throw new UnauthorizedError2({ message: "Authentication failed" });
11947
12279
  }
11948
12280
  const { user, role, locale } = await resolveAuthenticatedUser(keyRecord.userId);
11949
- keysRepository3.updateLastUsedById(keyRecord.id).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
12281
+ keysRepository3.updateLastUsedById(keyRecord.id, readContextClientIdentity(c)).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
11950
12282
  c.set("auth", {
11951
12283
  user,
11952
12284
  userId: String(user.id),
@@ -11967,35 +12299,39 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
11967
12299
  userAgent: c.req.header("user-agent")
11968
12300
  });
11969
12301
  await next();
12302
+ return void 0;
11970
12303
  });
11971
12304
  var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
11972
- const profileVerifier = selectAuthProfile(c);
11973
- if (profileVerifier !== null) {
11974
- c.set("auth", await profileVerifier.verify(c));
12305
+ const profile = await runAuthProfile(c);
12306
+ if (profile.kind === "refused") {
12307
+ return profile.response;
12308
+ }
12309
+ if (profile.kind === "authenticated") {
12310
+ c.set("auth", profile.auth);
11975
12311
  await next();
11976
- return;
12312
+ return void 0;
11977
12313
  }
11978
12314
  const authHeader = c.req.header("Authorization");
11979
12315
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
11980
12316
  await next();
11981
- return;
12317
+ return void 0;
11982
12318
  }
11983
12319
  const token = authHeader.substring(7);
11984
12320
  try {
11985
12321
  const decoded = decodeToken2(token);
11986
12322
  if (!decoded || !decoded.keyId) {
11987
12323
  await next();
11988
- return;
12324
+ return void 0;
11989
12325
  }
11990
12326
  const keyId = decoded.keyId;
11991
12327
  const keyRecord = await keysRepository3.findActiveByKeyId(keyId);
11992
12328
  if (!keyRecord) {
11993
12329
  await next();
11994
- return;
12330
+ return void 0;
11995
12331
  }
11996
12332
  if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
11997
12333
  await next();
11998
- return;
12334
+ return void 0;
11999
12335
  }
12000
12336
  verifyClientToken2(
12001
12337
  token,
@@ -12008,10 +12344,10 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
12008
12344
  ]);
12009
12345
  if (!result || result.user.status !== "active") {
12010
12346
  await next();
12011
- return;
12347
+ return void 0;
12012
12348
  }
12013
12349
  const { user, role } = result;
12014
- keysRepository3.updateLastUsedById(keyRecord.id).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
12350
+ keysRepository3.updateLastUsedById(keyRecord.id, readContextClientIdentity(c)).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
12015
12351
  c.set("auth", {
12016
12352
  user,
12017
12353
  userId: String(user.id),
@@ -12023,6 +12359,7 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
12023
12359
  } catch {
12024
12360
  }
12025
12361
  await next();
12362
+ return void 0;
12026
12363
  }, { skips: ["auth"] });
12027
12364
 
12028
12365
  // src/server/middleware/require-permission.ts
@@ -12487,7 +12824,7 @@ var userRouter = defineRouter3({
12487
12824
  // src/server/routes/oauth/index.ts
12488
12825
  init_esm();
12489
12826
 
12490
- // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/url.js
12827
+ // ../../node_modules/.pnpm/hono@4.13.0/node_modules/hono/dist/utils/url.js
12491
12828
  var tryDecode = (str, decoder) => {
12492
12829
  try {
12493
12830
  return decoder(str);
@@ -12501,10 +12838,12 @@ var tryDecode = (str, decoder) => {
12501
12838
  });
12502
12839
  }
12503
12840
  };
12841
+ var tryDecodeURIComponent = (str) => str.indexOf("%") !== -1 ? tryDecode(str, decodeURIComponent_) : str;
12504
12842
  var decodeURIComponent_ = decodeURIComponent;
12505
12843
 
12506
- // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/cookie.js
12844
+ // ../../node_modules/.pnpm/hono@4.13.0/node_modules/hono/dist/utils/cookie.js
12507
12845
  var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
12846
+ var relaxedCookieNameRegEx = /^[!#-:<>-[\]-~]+$/;
12508
12847
  var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
12509
12848
  var trimCookieWhitespace = (value) => {
12510
12849
  let start = 0;
@@ -12537,7 +12876,7 @@ var parse = (cookie, name) => {
12537
12876
  continue;
12538
12877
  }
12539
12878
  const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
12540
- if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) {
12879
+ if (name && name !== cookieName || !relaxedCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) {
12541
12880
  continue;
12542
12881
  }
12543
12882
  let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
@@ -12545,7 +12884,7 @@ var parse = (cookie, name) => {
12545
12884
  cookieValue = cookieValue.slice(1, -1);
12546
12885
  }
12547
12886
  if (validCookieValueRegEx.test(cookieValue)) {
12548
- parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? tryDecode(cookieValue, decodeURIComponent_) : cookieValue;
12887
+ parsedCookie[cookieName] = tryDecodeURIComponent(cookieValue);
12549
12888
  if (name) {
12550
12889
  break;
12551
12890
  }
@@ -12624,7 +12963,7 @@ var serialize = (name, value, opt) => {
12624
12963
  return _serialize(name, value, opt);
12625
12964
  };
12626
12965
 
12627
- // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/helper/cookie/index.js
12966
+ // ../../node_modules/.pnpm/hono@4.13.0/node_modules/hono/dist/helper/cookie/index.js
12628
12967
  var getCookie = (c, key, prefix) => {
12629
12968
  const cookie = c.req.raw.headers.get("Cookie");
12630
12969
  if (typeof key === "string") {
@@ -13712,6 +14051,7 @@ export {
13712
14051
  roles,
13713
14052
  rolesRepository,
13714
14053
  rotateKeyService,
14054
+ runAuthProfile,
13715
14055
  runBeforeRegister,
13716
14056
  sealSession,
13717
14057
  selectAuthProfile,