deepspace 0.34.0 → 0.35.0

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/expo.d.ts CHANGED
@@ -19,7 +19,7 @@ interface ExpoAuthStorage {
19
19
  deleteItem(key: string): Promise<void>;
20
20
  }
21
21
  interface DeepSpaceExpoClientOptions {
22
- /** Deployed app origin, for example `https://veriluma.app.space`. */
22
+ /** Deployed app origin, for example `https://example.app.space`. */
23
23
  baseUrl: string;
24
24
  /** Defaults to Expo's `createURL('auth/callback')`. */
25
25
  redirectUri?: string;
@@ -45,6 +45,16 @@ declare class DeepSpaceExpoError extends Error {
45
45
  readonly body: unknown;
46
46
  constructor(message: string, status: number, body: unknown);
47
47
  }
48
+ /**
49
+ * Thrown by `signIn` when the person closes the sign-in sheet. It isn't a
50
+ * failure, so apps usually return to where they were without showing an error.
51
+ * Check `code`: `instanceof` fails if the bundler includes two copies of
52
+ * this package.
53
+ */
54
+ declare class DeepSpaceSignInCancelledError extends Error {
55
+ readonly code: "sign_in_cancelled";
56
+ constructor();
57
+ }
48
58
  declare function createDeepSpaceExpoClient(options: DeepSpaceExpoClientOptions): DeepSpaceExpoClient;
49
59
  declare class DeepSpaceExpoClient {
50
60
  private readonly baseUrl;
@@ -70,4 +80,4 @@ declare class DeepSpaceExpoClient {
70
80
  private fetchWithTimeout;
71
81
  }
72
82
 
73
- export { DeepSpaceExpoClient, type DeepSpaceExpoClientOptions, DeepSpaceExpoError, type DeepSpaceExpoSession, type DeepSpaceExpoUser, type ExpoAuthPaths, type ExpoAuthProvider, type ExpoAuthStorage, buildExpoOAuthStartUrl, codeFromExpoRedirect, createDeepSpaceExpoClient, normalizeExpoBaseUrl, stateFromExpoRedirect };
83
+ export { DeepSpaceExpoClient, type DeepSpaceExpoClientOptions, DeepSpaceExpoError, type DeepSpaceExpoSession, type DeepSpaceExpoUser, DeepSpaceSignInCancelledError, type ExpoAuthPaths, type ExpoAuthProvider, type ExpoAuthStorage, buildExpoOAuthStartUrl, codeFromExpoRedirect, createDeepSpaceExpoClient, normalizeExpoBaseUrl, stateFromExpoRedirect };
package/dist/expo.js CHANGED
@@ -45,6 +45,13 @@ var DeepSpaceExpoError = class extends Error {
45
45
  this.body = body;
46
46
  }
47
47
  };
48
+ var DeepSpaceSignInCancelledError = class extends Error {
49
+ code = "sign_in_cancelled";
50
+ constructor() {
51
+ super("DeepSpace sign-in was cancelled");
52
+ this.name = "DeepSpaceSignInCancelledError";
53
+ }
54
+ };
48
55
  var DEFAULT_PATHS = {
49
56
  start: "/api/auth/native-start",
50
57
  exchange: "/api/auth/native-exchange",
@@ -105,7 +112,8 @@ var DeepSpaceExpoClient = class {
105
112
  codeChallenge
106
113
  );
107
114
  const result = await WebBrowser.openAuthSessionAsync(startUrl, this.redirectUri);
108
- if (result.type !== "success" || !result.url) throw new Error("DeepSpace sign-in was cancelled");
115
+ if (result.type === "cancel" || result.type === "dismiss") throw new DeepSpaceSignInCancelledError();
116
+ if (result.type !== "success" || !result.url) throw new Error("DeepSpace sign-in did not finish");
109
117
  const code = codeFromExpoRedirect(result.url);
110
118
  if (!code) throw new Error("DeepSpace sign-in returned no exchange code");
111
119
  if (stateFromExpoRedirect(result.url) !== expectedState) {
@@ -224,6 +232,7 @@ function isReplayableBody(body) {
224
232
  export {
225
233
  DeepSpaceExpoClient,
226
234
  DeepSpaceExpoError,
235
+ DeepSpaceSignInCancelledError,
227
236
  buildExpoOAuthStartUrl,
228
237
  codeFromExpoRedirect,
229
238
  createDeepSpaceExpoClient,
package/dist/expo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/expo.ts","../src/expo-url.ts"],"sourcesContent":["/**\n * DeepSpace Expo client.\n *\n * This entry is intentionally separate from `deepspace`: the browser client\n * uses Better Auth cookies and browser globals, while an Expo app needs the\n * system browser, a custom-scheme callback, and SecureStore-backed sessions.\n * The Worker still owns auth and JWT issuance; this package only transports the\n * one-time OAuth code and refreshes the short-lived bearer.\n */\nimport * as Linking from 'expo-linking'\nimport * as Crypto from 'expo-crypto'\nimport * as SecureStore from 'expo-secure-store'\nimport * as WebBrowser from 'expo-web-browser'\nimport {\n buildExpoOAuthStartUrl,\n codeFromExpoRedirect,\n normalizeExpoBaseUrl,\n stateFromExpoRedirect,\n} from './expo-url'\n\nexport type ExpoAuthProvider = 'google' | (string & {})\n\nexport interface DeepSpaceExpoSession {\n sessionToken: string\n accessToken: string\n}\n\nexport interface DeepSpaceExpoUser {\n userId: string\n claims: Record<string, unknown>\n}\n\nexport interface ExpoAuthStorage {\n getItem(key: string): Promise<string | null>\n setItem(key: string, value: string): Promise<void>\n deleteItem(key: string): Promise<void>\n}\n\nexport interface DeepSpaceExpoClientOptions {\n /** Deployed app origin, for example `https://veriluma.app.space`. */\n baseUrl: string\n /** Defaults to Expo's `createURL('auth/callback')`. */\n redirectUri?: string\n /** SecureStore-backed by default; inject a test store when unit testing. */\n storage?: ExpoAuthStorage\n /** Namespaced storage key so multiple apps can share one device. */\n storageKey?: string\n /** Worker endpoint overrides for apps with a custom auth route. */\n paths?: Partial<ExpoAuthPaths>\n fetch?: typeof globalThis.fetch\n /** Abort stalled auth/API requests after this many milliseconds. */\n timeoutMs?: number\n}\n\nexport interface ExpoAuthPaths {\n start: string\n exchange: string\n token: string\n me: string\n signOut: string\n}\n\nexport class DeepSpaceExpoError extends Error {\n readonly status: number\n readonly body: unknown\n\n constructor(message: string, status: number, body: unknown) {\n super(message)\n this.name = 'DeepSpaceExpoError'\n this.status = status\n this.body = body\n }\n}\n\nconst DEFAULT_PATHS: ExpoAuthPaths = {\n start: '/api/auth/native-start',\n exchange: '/api/auth/native-exchange',\n token: '/api/auth/native-token',\n me: '/api/auth/native-me',\n signOut: '/api/auth/native-signout',\n}\n\nconst defaultStorage: ExpoAuthStorage = {\n getItem: (key) => SecureStore.getItemAsync(key),\n setItem: (key, value) => SecureStore.setItemAsync(key, value),\n deleteItem: (key) => SecureStore.deleteItemAsync(key),\n}\n\nexport function createDeepSpaceExpoClient(options: DeepSpaceExpoClientOptions): DeepSpaceExpoClient {\n return new DeepSpaceExpoClient(options)\n}\n\nexport class DeepSpaceExpoClient {\n private readonly baseUrl: string\n private readonly redirectUri: string\n private readonly storage: ExpoAuthStorage\n private readonly storageKey: string\n private readonly paths: ExpoAuthPaths\n private readonly fetcher: typeof globalThis.fetch\n private readonly timeoutMs: number\n private session: DeepSpaceExpoSession | null | undefined\n\n constructor(options: DeepSpaceExpoClientOptions) {\n if (!options.baseUrl) throw new Error('DeepSpace Expo client requires baseUrl')\n this.baseUrl = normalizeExpoBaseUrl(options.baseUrl)\n this.redirectUri = options.redirectUri ?? Linking.createURL('auth/callback')\n this.storage = options.storage ?? defaultStorage\n this.storageKey = options.storageKey ?? `deepspace.session.${encodeURIComponent(this.baseUrl)}`\n this.paths = { ...DEFAULT_PATHS, ...options.paths }\n this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis)\n this.timeoutMs = options.timeoutMs ?? 15_000\n }\n\n async getSession(): Promise<DeepSpaceExpoSession | null> {\n if (this.session !== undefined) return this.session\n const raw = await this.storage.getItem(this.storageKey)\n if (!raw) return (this.session = null)\n try {\n const value = JSON.parse(raw) as Partial<DeepSpaceExpoSession>\n if (typeof value.sessionToken !== 'string' || typeof value.accessToken !== 'string') throw new Error('invalid session')\n return (this.session = value as DeepSpaceExpoSession)\n } catch {\n await this.storage.deleteItem(this.storageKey)\n return (this.session = null)\n }\n }\n\n async signIn(provider: ExpoAuthProvider = 'google', state?: string): Promise<DeepSpaceExpoUser> {\n const expectedState = state ?? createOAuthState()\n const codeVerifier = createCodeVerifier()\n const codeChallenge = await createCodeChallenge(codeVerifier)\n const startUrl = buildExpoOAuthStartUrl(\n this.baseUrl,\n provider,\n this.redirectUri,\n expectedState,\n this.paths.start,\n codeChallenge,\n )\n const result = await WebBrowser.openAuthSessionAsync(startUrl, this.redirectUri)\n if (result.type !== 'success' || !result.url) throw new Error('DeepSpace sign-in was cancelled')\n const code = codeFromExpoRedirect(result.url)\n if (!code) throw new Error('DeepSpace sign-in returned no exchange code')\n if (stateFromExpoRedirect(result.url) !== expectedState) {\n throw new Error('DeepSpace sign-in returned an invalid state')\n }\n const session = await this.json<DeepSpaceExpoSession>(this.paths.exchange, {\n method: 'POST',\n body: JSON.stringify({ code, code_verifier: codeVerifier }),\n })\n await this.saveSession(session)\n return this.me()\n }\n\n signInWithGoogle(state?: string): Promise<DeepSpaceExpoUser> { return this.signIn('google', state) }\n\n async refresh(): Promise<string | null> {\n const session = await this.getSession()\n if (!session) return null\n try {\n const next = await this.json<{ accessToken?: string; token?: string }>(this.paths.token, { method: 'POST', body: JSON.stringify({ sessionToken: session.sessionToken }) })\n const accessToken = next.accessToken ?? next.token\n if (!accessToken) throw new Error('DeepSpace did not issue an access token')\n await this.saveSession({ ...session, accessToken })\n return accessToken\n } catch (error) {\n if (error instanceof DeepSpaceExpoError && error.status === 401) await this.clearSession()\n return null\n }\n }\n\n async getAuthToken(): Promise<string | null> {\n const session = await this.getSession()\n return session?.accessToken || (await this.refresh())\n }\n\n async me(): Promise<DeepSpaceExpoUser> {\n const result = await this.request<DeepSpaceExpoUser>(this.paths.me)\n return result\n }\n\n async request<T>(path: string, init: RequestInit = {}, retried = false): Promise<T> {\n const token = await this.getAuthToken()\n const headers = new Headers(init.headers)\n if (!headers.has('Content-Type') && init.body != null && isJsonBody(init.body)) {\n headers.set('Content-Type', 'application/json')\n }\n if (token) headers.set('Authorization', `Bearer ${token}`)\n const response = await this.fetchWithTimeout(`${this.baseUrl}${path}`, { ...init, headers })\n const body = await response.json().catch(() => ({})) as T & { error?: string }\n if (response.status === 401 && !retried && isReplayableBody(init.body) && (await this.getSession())?.sessionToken) {\n const refreshed = await this.refresh()\n if (refreshed) return this.request<T>(path, init, true)\n }\n if (!response.ok) throw new DeepSpaceExpoError(typeof body.error === 'string' ? body.error : `DeepSpace request failed (${response.status})`, response.status, body)\n return body\n }\n\n async signOut(): Promise<void> {\n const session = await this.getSession()\n if (session) {\n await this.fetchWithTimeout(`${this.baseUrl}${this.paths.signOut}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: session.sessionToken }) }).catch(() => undefined)\n }\n await this.clearSession()\n }\n\n private async json<T>(path: string, init: RequestInit): Promise<T> {\n const response = await this.fetchWithTimeout(`${this.baseUrl}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}) } })\n const body = await response.json().catch(() => ({})) as T & { error?: string }\n if (!response.ok) throw new DeepSpaceExpoError(typeof body.error === 'string' ? body.error : `DeepSpace auth failed (${response.status})`, response.status, body)\n return body\n }\n\n private async saveSession(session: DeepSpaceExpoSession) { this.session = session; await this.storage.setItem(this.storageKey, JSON.stringify(session)) }\n private async clearSession() { this.session = null; await this.storage.deleteItem(this.storageKey) }\n\n private async fetchWithTimeout(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {\n if (!this.timeoutMs || this.timeoutMs <= 0) return this.fetcher(input, init)\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n const signal = init.signal\n if (signal) {\n if (signal.aborted) controller.abort()\n else signal.addEventListener('abort', () => controller.abort(), { once: true })\n }\n try {\n return await this.fetcher(input, { ...init, signal: controller.signal })\n } finally {\n clearTimeout(timer)\n }\n }\n}\n\nexport {\n buildExpoOAuthStartUrl,\n codeFromExpoRedirect,\n normalizeExpoBaseUrl,\n stateFromExpoRedirect,\n}\n\nfunction createOAuthState(): string {\n return Crypto.randomUUID()\n}\n\nfunction createCodeVerifier(): string {\n return `${Crypto.randomUUID().replaceAll('-', '')}${Crypto.randomUUID().replaceAll('-', '')}`\n}\n\nasync function createCodeChallenge(verifier: string): Promise<string> {\n const digest = await Crypto.digestStringAsync(\n Crypto.CryptoDigestAlgorithm.SHA256,\n verifier,\n { encoding: Crypto.CryptoEncoding.BASE64 },\n )\n return digest.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')\n}\n\nfunction isJsonBody(body: BodyInit): boolean {\n return typeof body === 'string'\n}\n\nfunction isReplayableBody(body: BodyInit | null | undefined): boolean {\n if (body == null || typeof body === 'string') return true\n if (typeof FormData !== 'undefined' && body instanceof FormData) return true\n if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) return true\n if (typeof Blob !== 'undefined' && body instanceof Blob) return true\n if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return true\n return false\n}\n","/** Pure URL helpers shared by the Expo auth client and its tests. */\nexport function buildExpoOAuthStartUrl(\n baseUrl: string,\n provider: string,\n redirectUri: string,\n state?: string,\n startPath = '/api/auth/native-start',\n codeChallenge?: string,\n): string {\n const url = new URL(startPath, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`)\n url.searchParams.set('provider', provider)\n url.searchParams.set('redirect_uri', redirectUri)\n if (state) url.searchParams.set('state', state)\n if (codeChallenge) {\n url.searchParams.set('code_challenge', codeChallenge)\n url.searchParams.set('code_challenge_method', 'S256')\n }\n return url.toString()\n}\n\nexport function codeFromExpoRedirect(url: string): string | null {\n try {\n return new URL(url).searchParams.get('code')\n } catch {\n return null\n }\n}\n\nexport function stateFromExpoRedirect(url: string): string | null {\n try {\n return new URL(url).searchParams.get('state')\n } catch {\n return null\n }\n}\n\nexport function normalizeExpoBaseUrl(baseUrl: string): string {\n return baseUrl.replace(/\\/+$/, '')\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/expo.ts","../src/expo-url.ts"],"sourcesContent":["/**\n * DeepSpace Expo client.\n *\n * This entry is intentionally separate from `deepspace`: the browser client\n * uses Better Auth cookies and browser globals, while an Expo app needs the\n * system browser, a custom-scheme callback, and SecureStore-backed sessions.\n * The Worker still owns auth and JWT issuance; this package only transports the\n * one-time OAuth code and refreshes the short-lived bearer.\n */\nimport * as Linking from 'expo-linking'\nimport * as Crypto from 'expo-crypto'\nimport * as SecureStore from 'expo-secure-store'\nimport * as WebBrowser from 'expo-web-browser'\nimport {\n buildExpoOAuthStartUrl,\n codeFromExpoRedirect,\n normalizeExpoBaseUrl,\n stateFromExpoRedirect,\n} from './expo-url'\n\nexport type ExpoAuthProvider = 'google' | (string & {})\n\nexport interface DeepSpaceExpoSession {\n sessionToken: string\n accessToken: string\n}\n\nexport interface DeepSpaceExpoUser {\n userId: string\n claims: Record<string, unknown>\n}\n\nexport interface ExpoAuthStorage {\n getItem(key: string): Promise<string | null>\n setItem(key: string, value: string): Promise<void>\n deleteItem(key: string): Promise<void>\n}\n\nexport interface DeepSpaceExpoClientOptions {\n /** Deployed app origin, for example `https://example.app.space`. */\n baseUrl: string\n /** Defaults to Expo's `createURL('auth/callback')`. */\n redirectUri?: string\n /** SecureStore-backed by default; inject a test store when unit testing. */\n storage?: ExpoAuthStorage\n /** Namespaced storage key so multiple apps can share one device. */\n storageKey?: string\n /** Worker endpoint overrides for apps with a custom auth route. */\n paths?: Partial<ExpoAuthPaths>\n fetch?: typeof globalThis.fetch\n /** Abort stalled auth/API requests after this many milliseconds. */\n timeoutMs?: number\n}\n\nexport interface ExpoAuthPaths {\n start: string\n exchange: string\n token: string\n me: string\n signOut: string\n}\n\nexport class DeepSpaceExpoError extends Error {\n readonly status: number\n readonly body: unknown\n\n constructor(message: string, status: number, body: unknown) {\n super(message)\n this.name = 'DeepSpaceExpoError'\n this.status = status\n this.body = body\n }\n}\n\n/**\n * Thrown by `signIn` when the person closes the sign-in sheet. It isn't a\n * failure, so apps usually return to where they were without showing an error.\n * Check `code`: `instanceof` fails if the bundler includes two copies of\n * this package.\n */\nexport class DeepSpaceSignInCancelledError extends Error {\n readonly code = 'sign_in_cancelled' as const\n\n constructor() {\n super('DeepSpace sign-in was cancelled')\n this.name = 'DeepSpaceSignInCancelledError'\n }\n}\n\nconst DEFAULT_PATHS: ExpoAuthPaths = {\n start: '/api/auth/native-start',\n exchange: '/api/auth/native-exchange',\n token: '/api/auth/native-token',\n me: '/api/auth/native-me',\n signOut: '/api/auth/native-signout',\n}\n\nconst defaultStorage: ExpoAuthStorage = {\n getItem: (key) => SecureStore.getItemAsync(key),\n setItem: (key, value) => SecureStore.setItemAsync(key, value),\n deleteItem: (key) => SecureStore.deleteItemAsync(key),\n}\n\nexport function createDeepSpaceExpoClient(options: DeepSpaceExpoClientOptions): DeepSpaceExpoClient {\n return new DeepSpaceExpoClient(options)\n}\n\nexport class DeepSpaceExpoClient {\n private readonly baseUrl: string\n private readonly redirectUri: string\n private readonly storage: ExpoAuthStorage\n private readonly storageKey: string\n private readonly paths: ExpoAuthPaths\n private readonly fetcher: typeof globalThis.fetch\n private readonly timeoutMs: number\n private session: DeepSpaceExpoSession | null | undefined\n\n constructor(options: DeepSpaceExpoClientOptions) {\n if (!options.baseUrl) throw new Error('DeepSpace Expo client requires baseUrl')\n this.baseUrl = normalizeExpoBaseUrl(options.baseUrl)\n this.redirectUri = options.redirectUri ?? Linking.createURL('auth/callback')\n this.storage = options.storage ?? defaultStorage\n this.storageKey = options.storageKey ?? `deepspace.session.${encodeURIComponent(this.baseUrl)}`\n this.paths = { ...DEFAULT_PATHS, ...options.paths }\n this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis)\n this.timeoutMs = options.timeoutMs ?? 15_000\n }\n\n async getSession(): Promise<DeepSpaceExpoSession | null> {\n if (this.session !== undefined) return this.session\n const raw = await this.storage.getItem(this.storageKey)\n if (!raw) return (this.session = null)\n try {\n const value = JSON.parse(raw) as Partial<DeepSpaceExpoSession>\n if (typeof value.sessionToken !== 'string' || typeof value.accessToken !== 'string') throw new Error('invalid session')\n return (this.session = value as DeepSpaceExpoSession)\n } catch {\n await this.storage.deleteItem(this.storageKey)\n return (this.session = null)\n }\n }\n\n async signIn(provider: ExpoAuthProvider = 'google', state?: string): Promise<DeepSpaceExpoUser> {\n const expectedState = state ?? createOAuthState()\n const codeVerifier = createCodeVerifier()\n const codeChallenge = await createCodeChallenge(codeVerifier)\n const startUrl = buildExpoOAuthStartUrl(\n this.baseUrl,\n provider,\n this.redirectUri,\n expectedState,\n this.paths.start,\n codeChallenge,\n )\n const result = await WebBrowser.openAuthSessionAsync(startUrl, this.redirectUri)\n if (result.type === 'cancel' || result.type === 'dismiss') throw new DeepSpaceSignInCancelledError()\n if (result.type !== 'success' || !result.url) throw new Error('DeepSpace sign-in did not finish')\n const code = codeFromExpoRedirect(result.url)\n if (!code) throw new Error('DeepSpace sign-in returned no exchange code')\n if (stateFromExpoRedirect(result.url) !== expectedState) {\n throw new Error('DeepSpace sign-in returned an invalid state')\n }\n const session = await this.json<DeepSpaceExpoSession>(this.paths.exchange, {\n method: 'POST',\n body: JSON.stringify({ code, code_verifier: codeVerifier }),\n })\n await this.saveSession(session)\n return this.me()\n }\n\n signInWithGoogle(state?: string): Promise<DeepSpaceExpoUser> { return this.signIn('google', state) }\n\n async refresh(): Promise<string | null> {\n const session = await this.getSession()\n if (!session) return null\n try {\n const next = await this.json<{ accessToken?: string; token?: string }>(this.paths.token, { method: 'POST', body: JSON.stringify({ sessionToken: session.sessionToken }) })\n const accessToken = next.accessToken ?? next.token\n if (!accessToken) throw new Error('DeepSpace did not issue an access token')\n await this.saveSession({ ...session, accessToken })\n return accessToken\n } catch (error) {\n if (error instanceof DeepSpaceExpoError && error.status === 401) await this.clearSession()\n return null\n }\n }\n\n async getAuthToken(): Promise<string | null> {\n const session = await this.getSession()\n return session?.accessToken || (await this.refresh())\n }\n\n async me(): Promise<DeepSpaceExpoUser> {\n const result = await this.request<DeepSpaceExpoUser>(this.paths.me)\n return result\n }\n\n async request<T>(path: string, init: RequestInit = {}, retried = false): Promise<T> {\n const token = await this.getAuthToken()\n const headers = new Headers(init.headers)\n if (!headers.has('Content-Type') && init.body != null && isJsonBody(init.body)) {\n headers.set('Content-Type', 'application/json')\n }\n if (token) headers.set('Authorization', `Bearer ${token}`)\n const response = await this.fetchWithTimeout(`${this.baseUrl}${path}`, { ...init, headers })\n const body = await response.json().catch(() => ({})) as T & { error?: string }\n if (response.status === 401 && !retried && isReplayableBody(init.body) && (await this.getSession())?.sessionToken) {\n const refreshed = await this.refresh()\n if (refreshed) return this.request<T>(path, init, true)\n }\n if (!response.ok) throw new DeepSpaceExpoError(typeof body.error === 'string' ? body.error : `DeepSpace request failed (${response.status})`, response.status, body)\n return body\n }\n\n async signOut(): Promise<void> {\n const session = await this.getSession()\n if (session) {\n await this.fetchWithTimeout(`${this.baseUrl}${this.paths.signOut}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionToken: session.sessionToken }) }).catch(() => undefined)\n }\n await this.clearSession()\n }\n\n private async json<T>(path: string, init: RequestInit): Promise<T> {\n const response = await this.fetchWithTimeout(`${this.baseUrl}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}) } })\n const body = await response.json().catch(() => ({})) as T & { error?: string }\n if (!response.ok) throw new DeepSpaceExpoError(typeof body.error === 'string' ? body.error : `DeepSpace auth failed (${response.status})`, response.status, body)\n return body\n }\n\n private async saveSession(session: DeepSpaceExpoSession) { this.session = session; await this.storage.setItem(this.storageKey, JSON.stringify(session)) }\n private async clearSession() { this.session = null; await this.storage.deleteItem(this.storageKey) }\n\n private async fetchWithTimeout(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {\n if (!this.timeoutMs || this.timeoutMs <= 0) return this.fetcher(input, init)\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n const signal = init.signal\n if (signal) {\n if (signal.aborted) controller.abort()\n else signal.addEventListener('abort', () => controller.abort(), { once: true })\n }\n try {\n return await this.fetcher(input, { ...init, signal: controller.signal })\n } finally {\n clearTimeout(timer)\n }\n }\n}\n\nexport {\n buildExpoOAuthStartUrl,\n codeFromExpoRedirect,\n normalizeExpoBaseUrl,\n stateFromExpoRedirect,\n}\n\nfunction createOAuthState(): string {\n return Crypto.randomUUID()\n}\n\nfunction createCodeVerifier(): string {\n return `${Crypto.randomUUID().replaceAll('-', '')}${Crypto.randomUUID().replaceAll('-', '')}`\n}\n\nasync function createCodeChallenge(verifier: string): Promise<string> {\n const digest = await Crypto.digestStringAsync(\n Crypto.CryptoDigestAlgorithm.SHA256,\n verifier,\n { encoding: Crypto.CryptoEncoding.BASE64 },\n )\n return digest.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')\n}\n\nfunction isJsonBody(body: BodyInit): boolean {\n return typeof body === 'string'\n}\n\nfunction isReplayableBody(body: BodyInit | null | undefined): boolean {\n if (body == null || typeof body === 'string') return true\n if (typeof FormData !== 'undefined' && body instanceof FormData) return true\n if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) return true\n if (typeof Blob !== 'undefined' && body instanceof Blob) return true\n if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return true\n return false\n}\n","/** Pure URL helpers shared by the Expo auth client and its tests. */\nexport function buildExpoOAuthStartUrl(\n baseUrl: string,\n provider: string,\n redirectUri: string,\n state?: string,\n startPath = '/api/auth/native-start',\n codeChallenge?: string,\n): string {\n const url = new URL(startPath, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`)\n url.searchParams.set('provider', provider)\n url.searchParams.set('redirect_uri', redirectUri)\n if (state) url.searchParams.set('state', state)\n if (codeChallenge) {\n url.searchParams.set('code_challenge', codeChallenge)\n url.searchParams.set('code_challenge_method', 'S256')\n }\n return url.toString()\n}\n\nexport function codeFromExpoRedirect(url: string): string | null {\n try {\n return new URL(url).searchParams.get('code')\n } catch {\n return null\n }\n}\n\nexport function stateFromExpoRedirect(url: string): string | null {\n try {\n return new URL(url).searchParams.get('state')\n } catch {\n return null\n }\n}\n\nexport function normalizeExpoBaseUrl(baseUrl: string): string {\n return baseUrl.replace(/\\/+$/, '')\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -141,7 +141,7 @@ interface YjsSubscription {
141
141
  * 3. Server-side process.env.DEEPSPACE_ENV
142
142
  * 4. Hostname detection (fallback)
143
143
  */
144
- type Environment = 'dev' | 'staging' | 'prod';
144
+ type Environment = 'dev' | 'staging' | 'medical' | 'prod';
145
145
  interface EnvironmentConfig {
146
146
  name: Environment;
147
147
  /** Platform API worker URL */
@@ -158,6 +158,8 @@ interface EnvironmentConfig {
158
158
  mainAppUrl: string;
159
159
  /** Builder dashboard URL */
160
160
  dashboardUrl: string;
161
+ /** Privacy policy that sign-in screens link to */
162
+ privacyPolicyUrl: string;
161
163
  }
162
164
  declare function detectEnvironment(): Environment;
163
165
  declare function getEnvironmentConfig(): EnvironmentConfig;
@@ -165,6 +167,8 @@ declare function getApiUrl(): string;
165
167
  declare function getPlatformWorkerUrl(): string;
166
168
  declare function getAuthUrl(): string;
167
169
  declare function isLocalDev(): boolean;
170
+ /** True only on production. DeepSpace Medical (`medical`) is its own plane,
171
+ * so this is false there; check `detectEnvironment()` to tell the planes apart. */
168
172
  declare function isProduction(): boolean;
169
173
  /** Reset cached environment (useful for testing) */
170
174
  declare function resetEnvironmentCache(): void;
package/dist/index.js CHANGED
@@ -1,3 +1,44 @@
1
+ // src/shared/planes.ts
2
+ var PLANES = {
3
+ production: {
4
+ auth: "https://auth.deep.space",
5
+ api: "https://api-worker.deep.space",
6
+ platform: "https://platform-worker.deep.space",
7
+ deploy: "https://deploy-worker.deep.space",
8
+ appDomain: "app.space",
9
+ platformDomain: "deep.space",
10
+ home: "https://deep.space",
11
+ dashboard: "https://dashboard.deep.space",
12
+ privacyPolicy: "https://deep.space/privacy"
13
+ },
14
+ // Services on deepspacesites.com, apps on spacestest.com: production's
15
+ // two-zone split, because a Worker cannot fetch one in its own zone.
16
+ staging: {
17
+ auth: "https://auth.deepspacesites.com",
18
+ api: "https://api.deepspacesites.com",
19
+ platform: "https://platform.deepspacesites.com",
20
+ deploy: "https://deploy.deepspacesites.com",
21
+ appDomain: "spacestest.com",
22
+ platformDomain: "deepspacesites.com",
23
+ home: "https://spacestest.com",
24
+ dashboard: "https://dashboard.deepspacesites.com",
25
+ privacyPolicy: "https://deep.space/privacy"
26
+ },
27
+ // Services on deepspacemedical.com, apps on deepspacemedical.app: two zones
28
+ // in the medical Cloudflare account. The plane has no dashboard.
29
+ medical: {
30
+ auth: "https://auth.deepspacemedical.com",
31
+ api: "https://api.deepspacemedical.com",
32
+ platform: "https://platform.deepspacemedical.com",
33
+ deploy: "https://deploy.deepspacemedical.com",
34
+ appDomain: "deepspacemedical.app",
35
+ platformDomain: "deepspacemedical.com",
36
+ home: "https://deepspacemedical.com",
37
+ dashboard: null,
38
+ privacyPolicy: "https://deepspacemedical.com/privacy"
39
+ }
40
+ };
41
+
1
42
  // src/shared/env/index.ts
2
43
  var DEV_CONFIG = {
3
44
  name: "dev",
@@ -7,34 +48,33 @@ var DEV_CONFIG = {
7
48
  authSignInUrl: "http://localhost:5173/sign-in",
8
49
  authSignUpUrl: "http://localhost:5173/sign-up",
9
50
  mainAppUrl: "http://localhost:5173",
10
- dashboardUrl: "http://localhost:5174"
11
- };
12
- var STAGING_CONFIG = {
13
- name: "staging",
14
- apiUrl: "https://api.deepspacesites.com",
15
- platformWorkerUrl: "https://platform.deepspacesites.com",
16
- authUrl: "https://auth.deepspacesites.com",
17
- authSignInUrl: "https://auth.deepspacesites.com/login/social",
18
- authSignUpUrl: "https://auth.deepspacesites.com/login/social",
19
- mainAppUrl: "https://spacestest.com",
20
- dashboardUrl: "https://dashboard.deepspacesites.com"
21
- };
22
- var PROD_CONFIG = {
23
- name: "prod",
24
- apiUrl: "https://api-worker.deep.space",
25
- platformWorkerUrl: "https://platform-worker.deep.space",
26
- authUrl: "https://auth.deep.space",
27
- authSignInUrl: "https://auth.deep.space/login/social",
28
- authSignUpUrl: "https://auth.deep.space/login/social",
29
- mainAppUrl: "https://deep.space",
30
- dashboardUrl: "https://dashboard.deep.space"
51
+ dashboardUrl: "http://localhost:5174",
52
+ privacyPolicyUrl: "https://deep.space/privacy"
31
53
  };
54
+ function planeConfig(name, plane) {
55
+ return {
56
+ name,
57
+ apiUrl: plane.api,
58
+ platformWorkerUrl: plane.platform,
59
+ authUrl: plane.auth,
60
+ authSignInUrl: `${plane.auth}/login/social`,
61
+ authSignUpUrl: `${plane.auth}/login/social`,
62
+ mainAppUrl: plane.home,
63
+ // DeepSpace Medical has no dashboard; its home page stands in.
64
+ dashboardUrl: plane.dashboard ?? plane.home,
65
+ privacyPolicyUrl: plane.privacyPolicy
66
+ };
67
+ }
68
+ var STAGING_CONFIG = planeConfig("staging", PLANES.staging);
69
+ var MEDICAL_CONFIG = planeConfig("medical", PLANES.medical);
70
+ var PROD_CONFIG = planeConfig("prod", PLANES.production);
32
71
  var cachedEnvironment = null;
33
72
  function parseEnv(value) {
34
73
  if (!value) return null;
35
74
  const v = value.toLowerCase();
36
75
  if (v === "dev" || v === "development") return "dev";
37
76
  if (v === "staging") return "staging";
77
+ if (v === "medical") return "medical";
38
78
  if (v === "prod" || v === "production") return "prod";
39
79
  return null;
40
80
  }
@@ -75,13 +115,18 @@ function detectEnvironment() {
75
115
  cachedEnvironment = "dev";
76
116
  return "dev";
77
117
  }
78
- if (hostname === "spacestest.com" || hostname.endsWith(".spacestest.com") || hostname === "deepspacesites.com" || hostname.endsWith(".deepspacesites.com")) {
79
- cachedEnvironment = "staging";
80
- return "staging";
81
- }
82
- if (hostname === "app.space" || hostname.endsWith(".app.space") || hostname === "deep.space" || hostname.endsWith(".deep.space")) {
83
- cachedEnvironment = "prod";
84
- return "prod";
118
+ const onPlane = (plane) => [plane.appDomain, plane.platformDomain].some(
119
+ (zone) => hostname === zone || hostname.endsWith(`.${zone}`)
120
+ );
121
+ for (const [env, plane] of [
122
+ ["staging", PLANES.staging],
123
+ ["medical", PLANES.medical],
124
+ ["prod", PLANES.production]
125
+ ]) {
126
+ if (onPlane(plane)) {
127
+ cachedEnvironment = env;
128
+ return env;
129
+ }
85
130
  }
86
131
  }
87
132
  cachedEnvironment = "prod";
@@ -89,7 +134,16 @@ function detectEnvironment() {
89
134
  }
90
135
  function getEnvironmentConfig() {
91
136
  const env = detectEnvironment();
92
- return env === "dev" ? DEV_CONFIG : env === "staging" ? STAGING_CONFIG : PROD_CONFIG;
137
+ switch (env) {
138
+ case "dev":
139
+ return DEV_CONFIG;
140
+ case "staging":
141
+ return STAGING_CONFIG;
142
+ case "medical":
143
+ return MEDICAL_CONFIG;
144
+ case "prod":
145
+ return PROD_CONFIG;
146
+ }
93
147
  }
94
148
  function getApiUrl() {
95
149
  return getEnvironmentConfig().apiUrl;
@@ -1135,7 +1189,7 @@ function AuthOverlay({
1135
1189
  /* @__PURE__ */ jsx2(
1136
1190
  "a",
1137
1191
  {
1138
- href: "https://deep.space/privacy",
1192
+ href: getEnvironmentConfig().privacyPolicyUrl,
1139
1193
  target: "_blank",
1140
1194
  rel: "noopener noreferrer",
1141
1195
  className: "underline hover:text-foreground",