1import { activeAppId } from "../runtime/config.ts";2import { getRuntime } from "../runtime/runtime.ts";3import { sharedFieldIdentityOrNull } from "../schema/shared-keyring.ts";4import { AccessError } from "./errors.ts";56/** App-scoped, non-secret Jazz principal identifier safe to copy between users. */7export type SharingIdentity = string & { readonly __lofiSharingIdentity: true };89const prefix = "lofi1";10const prefixWithFingerprint = "lofi2";1112/** A decoded sharing identity: the raw principal, and — when the identity13 * was minted by a shared-field-capable app — the account's public-key14 * fingerprint. A carried fingerprint pins the peer's key out-of-band, so the15 * sync server never gets a first-sight window for that relationship. */16export type SharingIdentityDetails = {17 userId: string;18 fingerprint?: string;19};2021/**22 * Encodes a raw Jazz principal as a versioned identity scoped to the active23 * sync location's app. Identities are exchanged between users of the same24 * store, so they carry the store's app id — the declared sink's when one is25 * enrolled, the compiled managed app's otherwise. With a fingerprint, the26 * identity also carries the account's shared-field key fingerprint.27 */28export function encodeSharingIdentity(userId: string, fingerprint?: string): SharingIdentity {29 if (!userId || /[\s:]/.test(userId)) {30 throw new AccessError("invalid-identity", "Jazz returned an invalid sharing identity.");31 }32 if (fingerprint === undefined) {33 return `${prefix}:${activeAppId()}:${userId}` as SharingIdentity;34 }35 if (!fingerprint || /[\s:]/.test(fingerprint)) {36 throw new AccessError("invalid-identity", "The identity fingerprint is malformed.");37 }38 return `${prefixWithFingerprint}:${activeAppId()}:${userId}:${fingerprint}` as SharingIdentity;39}4041/**42 * Validates an app-scoped sharing identity and returns its parts. Both43 * identity versions decode: `lofi1` yields the principal alone, `lofi2` adds44 * the fingerprint. Surrounding whitespace from copy/paste is tolerated;45 * whitespace inside the principal is rejected — a grant for a padded46 * principal would look active to the owner while never matching the47 * recipient's actual account.48 */49export function decodeSharingIdentityDetails(identity: string): SharingIdentityDetails {50 const [version, identityAppId, userId, fingerprint, extra] = identity.trim().split(":");51 const malformed = new AccessError(52 "invalid-identity",53 "That sharing identity belongs to another app or is malformed. Ask the recipient to copy it again from this app.",54 );55 if (identityAppId !== activeAppId() || !userId || /\s/.test(userId) || extra !== undefined) {56 throw malformed;57 }58 if (version === prefix) {59 if (fingerprint !== undefined) throw malformed;60 return { userId };61 }62 if (version === prefixWithFingerprint) {63 if (!fingerprint || /\s/.test(fingerprint)) throw malformed;64 return { userId, fingerprint };65 }66 throw malformed;67}6869/** Validates a sharing identity and returns its raw Jazz principal. */70export function decodeSharingIdentity(identity: string): string {71 return decodeSharingIdentityDetails(identity).userId;72}7374/** Returns the non-secret, app-scoped identity users may copy for shares. */75export async function sharingIdentity(): Promise<SharingIdentity> {76 const runtime = await getRuntime();77 const userId = runtime.db.getAuthState().session?.user_id;78 if (!userId) {79 throw new AccessError(80 "invalid-identity",81 "The current Jazz principal is not ready. Wait for account startup and try again.",82 );83 }84 // Once the shared-field identity is installed, minted identities carry the85 // fingerprint so recipients pin this account's key person-to-person.86 return encodeSharingIdentity(userId, sharedFieldIdentityOrNull()?.fingerprint);87}