(CloudKit JS
+
+
+
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js
index 47e518cc..313cf7dd 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js
+++ b/Examples/MistDemo/Sources/MistDemoKit/Resources/js/users.js
@@ -1,7 +1,7 @@
// users/caller · users/discover panel handlers. The deprecated
// users/lookup/email and users/lookup/id primitives are not exposed —
-// users/discover is Apple's supported replacement and handles both email
-// and record-name lookups (phone-number support tracked in #398).
+// users/discover is Apple's supported replacement and handles email,
+// phone-number, and record-name lookups.
const usersCallerStatus = document.getElementById('users-caller-status');
const usersCallerRaw = document.getElementById('users-caller-raw');
@@ -24,9 +24,10 @@ document.getElementById('users-caller-btn').addEventListener('click', async () =
document.getElementById('users-discover-btn').addEventListener('click', async () => {
const emails = csv(document.getElementById('users-discover-emails').value);
+ const phoneNumbers = csv(document.getElementById('users-discover-phone-numbers').value);
const userRecordNames = csv(document.getElementById('users-discover-record-names').value);
- if (emails.length === 0 && userRecordNames.length === 0) {
- setStatus(usersDiscoverStatus, 'Provide at least one email or record name.', 'error');
+ if (emails.length === 0 && phoneNumbers.length === 0 && userRecordNames.length === 0) {
+ setStatus(usersDiscoverStatus, 'Provide at least one email, phone number, or record name.', 'error');
return;
}
await runPanelOperation({
@@ -35,7 +36,7 @@ document.getElementById('users-discover-btn').addEventListener('click', async ()
label: 'Discover users',
fn: async () => {
if (currentMode === 'mistkit') {
- return await postJSON('/api/users/discover', { emails, userRecordNames });
+ return await postJSON('/api/users/discover', { emails, phoneNumbers, userRecordNames });
}
// CloudKit JS exposes per-item primitives — loop and aggregate
// to match the REST endpoint's batch shape.
@@ -48,6 +49,14 @@ document.getElementById('users-discover-btn').addEventListener('click', async ()
results.push({ email, error: error.message });
}
}
+ for (const phoneNumber of phoneNumbers) {
+ try {
+ const identity = await ckJsContainer().discoverUserIdentityWithPhoneNumber(phoneNumber);
+ results.push({ phoneNumber, identity });
+ } catch (error) {
+ results.push({ phoneNumber, error: error.message });
+ }
+ }
for (const recordName of userRecordNames) {
try {
const identity = await ckJsContainer().discoverUserIdentityWithUserRecordName(recordName);
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift
index 9704fd2b..35137849 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/CloudKitService+WebBackend+Users.swift
@@ -41,10 +41,12 @@ extension CloudKitService {
internal func webDiscoverUsers(
emails: [String],
+ phoneNumbers: [String],
userRecordNames: [String]
) async throws -> [UserIdentity] {
let lookupInfos =
emails.map { UserIdentityLookupInfo(emailAddress: $0) }
+ + phoneNumbers.map { UserIdentityLookupInfo(phoneNumber: $0) }
+ userRecordNames.map { UserIdentityLookupInfo(userRecordName: $0) }
return try await discoverUserIdentities(lookupInfos: lookupInfos)
}
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
index 2669072b..b4fa1f9c 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebBackend.swift
@@ -103,6 +103,7 @@ internal protocol WebBackend: Sendable {
func webDiscoverUsers(
emails: [String],
+ phoneNumbers: [String],
userRecordNames: [String]
) async throws -> [UserIdentity]
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift
index 46fd42d4..3c0889c4 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebRequests+Users.swift
@@ -33,22 +33,28 @@ internal import Foundation
// wrapper (`discoverUserIdentities`) operates on the public database with
// web-auth credentials regardless of the request's selected database.
extension WebRequests {
- /// `POST /api/users/discover` — discover user identities by email address
- /// and/or user record name. Either list may be omitted; an absent key
- /// decodes to an empty array.
+ /// `POST /api/users/discover` — discover user identities by email address,
+ /// phone number, and/or user record name. Any list may be omitted; an
+ /// absent key decodes to an empty array.
internal struct DiscoverUsers: Decodable {
private enum CodingKeys: String, CodingKey {
case emails
+ case phoneNumbers
case userRecordNames
}
internal let emails: [String]
+ internal let phoneNumbers: [String]
internal let userRecordNames: [String]
internal init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.emails =
try container.decodeIfPresent([String].self, forKey: .emails) ?? []
+ self.phoneNumbers =
+ try container.decodeIfPresent(
+ [String].self, forKey: .phoneNumbers
+ ) ?? []
self.userRecordNames =
try container.decodeIfPresent(
[String].self, forKey: .userRecordNames
diff --git a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift
index 977c1d7b..c12a220a 100644
--- a/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift
+++ b/Examples/MistDemo/Sources/MistDemoKit/Server/WebServer+Users.swift
@@ -37,7 +37,8 @@
/// operate on the public database with web-auth credentials, so neither
/// carries a `database` selector. The deprecated `lookup/email` and
/// `lookup/id` primitives are intentionally not exposed — `discover` is
- /// Apple's supported replacement and handles email + record-name lookups.
+ /// Apple's supported replacement and handles email, phone-number, and
+ /// record-name lookups.
internal func addUsersEndpoints(
api: RouterGroup
) {
@@ -66,7 +67,7 @@
}
/// `POST /api/users/discover` — discover user identities by email
- /// address and/or user record name.
+ /// address, phone number, and/or user record name.
private func addUsersDiscoverEndpoint(
api: RouterGroup
) {
@@ -83,6 +84,7 @@
let backend = try backendFactory.make(token)
let users = try await backend.webDiscoverUsers(
emails: body.emails,
+ phoneNumbers: body.phoneNumbers,
userRecordNames: body.userRecordNames
)
return try WebJSON.encoder().encode(
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift
index 299c1ad3..777b7f85 100644
--- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+Calls.swift
@@ -107,6 +107,7 @@
/// Captured arguments from the most recent `webDiscoverUsers` call.
internal struct DiscoverUsersCall: Sendable {
internal let emails: [String]
+ internal let phoneNumbers: [String]
internal let userRecordNames: [String]
}
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift
index 8b9861ef..772d2049 100644
--- a/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/MockBackend+UserOperations.swift
@@ -46,16 +46,23 @@
internal func webDiscoverUsers(
emails: [String],
+ phoneNumbers: [String],
userRecordNames: [String]
) async throws -> [UserIdentity] {
lastDiscoverUsers = DiscoverUsersCall(
emails: emails,
+ phoneNumbers: phoneNumbers,
userRecordNames: userRecordNames
)
try consumePendingError()
return emails.map { email in
UserIdentity(lookupInfo: UserIdentityLookupInfo(emailAddress: email))
}
+ + phoneNumbers.map { phoneNumber in
+ UserIdentity(
+ lookupInfo: UserIdentityLookupInfo(phoneNumber: phoneNumber)
+ )
+ }
+ userRecordNames.map { name in
UserIdentity(userRecordName: .recordName(name))
}
diff --git a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift
index 86b84c38..b13c265a 100644
--- a/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift
+++ b/Examples/MistDemo/Tests/MistDemoTests/Server/WebServerTests+Users.swift
@@ -91,13 +91,17 @@
}
@Test(
- "POST /api/users/discover forwards emails and record names to the backend"
+ """
+ POST /api/users/discover forwards emails, phone numbers, \
+ and record names to the backend
+ """
)
internal func usersDiscoverForwards() async throws {
let fixture = Self.makeFixture(authenticated: true)
let app = Application(router: try fixture.server.makeRouter())
let jsonBody = """
{"emails":["a@example.com","b@example.com"],\
+ "phoneNumbers":["+15555550123"],\
"userRecordNames":["_user-1"]}
"""
@@ -113,12 +117,13 @@
UsersPayload.self,
from: Data(response.body.readableBytesView)
)
- #expect(payload.users.count == 3)
+ #expect(payload.users.count == 4)
}
}
let captured = await fixture.backend.lastDiscoverUsers
#expect(captured?.emails == ["a@example.com", "b@example.com"])
+ #expect(captured?.phoneNumbers == ["+15555550123"])
#expect(captured?.userRecordNames == ["_user-1"])
}