Skip to content

feat: phone auth with auto SMS code retrieval#38

Merged
KalebCole merged 4 commits intomainfrom
sasha/phone-auth-flow
Mar 24, 2026
Merged

feat: phone auth with auto SMS code retrieval#38
KalebCole merged 4 commits intomainfrom
sasha/phone-auth-flow

Conversation

@KalebCole
Copy link
Copy Markdown
Owner

@KalebCole KalebCole commented Mar 24, 2026

What

Replaces the bookmarklet login with a proper phone-number auth flow that auto-retrieves SMS verification codes.

Before

partiful auth login
# → Starts local server on port 9876
# → User has to create a bookmarklet
# → User clicks bookmarklet on partiful.com
# → 4+ manual steps, confusing for non-developers

After

partiful auth login +12066993977
# → Sends SMS code automatically
# → Retrieves code from iMessage (macOS) or prompts
# → Authenticates in ~10 seconds, zero manual steps on macOS

How It Works

Reverse-engineered the Partiful login flow from the app bundle:

  1. POST /sendAuthCodeTrusted — sends SMS (no reCAPTCHA needed)
  2. User code retrieved via imsg CLI (macOS) or termux-sms-list (Android) or manual prompt
  3. POST /getLoginToken — exchanges phone + code for a custom JWT
  4. POST identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken — exchanges JWT for Firebase tokens
  5. Saves refreshToken to ~/.config/partiful/auth.json

Platform Detection

Platform Detection Code Retrieval
macOS + imsg CLI os.platform() === 'darwin' + which imsg Auto-poll iMessage for Partiful SMS
Android + Termux /system/build.prop exists + which termux-sms-list Auto-poll SMS inbox
Everything else Manual prompt

Options

  • --code <code> — provide code directly (skip SMS send)
  • --no-auto — disable auto-retrieval, always prompt manually

Tested

  • ✅ Full end-to-end on macOS with imsg — sent SMS, auto-retrieved code, authenticated, verified API calls work
  • ✅ All 49 existing tests pass
  • partiful events list returns 9 events with new auth tokens

Research

docs/research/2026-03-24-auth-flow-endpoints.md

Summary by CodeRabbit

Release Notes

  • New Features

    • SMS-based authentication with automatic code retrieval on macOS and Android; manual entry fallback available
    • Text blast messaging now fully functional with preview and confirmation workflow
  • Deprecations

    • Legacy CLI top-level commands are deprecated; use the new command structure (e.g., events list instead of list)
  • Documentation

    • Added API endpoint research documentation for authentication and text blast services

Replaced the stub with a working implementation based on browser
interception of partiful.com. The endpoint is:

  POST https://api.partiful.com/createTextBlast

Supports:
- Message text (max 480 chars)
- Target audience by guest status (GOING, MAYBE, DECLINED, etc.)
- Show/hide on event activity feed
- Dry-run mode for safe testing
- Interactive confirmation before sending (skip with --yes)
- Full input validation

Research doc: docs/research/2026-03-24-text-blast-endpoint.md
Replace the bookmarklet-based login with a proper phone auth flow:

1. sendAuthCodeTrusted(phone) → SMS sent (no reCAPTCHA needed)
2. Auto-retrieve code from iMessage (macOS) or Termux (Android)
3. getLoginToken(phone, code) → Partiful custom JWT
4. signInWithCustomToken → Firebase idToken + refreshToken
5. Save to ~/.config/partiful/auth.json

Platform detection:
- macOS + imsg CLI → auto-retrieves SMS code (tested, works)
- Android + termux-sms-list → auto-retrieves SMS code (untested)
- All other platforms → prompts for manual code entry

Usage:
  partiful auth login +12066993977        # auto-retrieve code
  partiful auth login +12066993977 --no-auto  # manual entry
  partiful auth login +12066993977 --code 123456  # skip SMS

Research: docs/research/2026-03-24-auth-flow-endpoints.md
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Warning

Rate limit exceeded

@KalebCole has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 708f180c-9985-4fe5-8b4f-accfda23801f

📥 Commits

Reviewing files that changed from the base of the PR and between e42362e and 760ab2d.

📒 Files selected for processing (2)
  • src/cli.js
  • src/commands/blasts.js
📝 Walkthrough

Walkthrough

Documentation added for Partiful authentication endpoints and text blast API. CLI upgraded with deprecated command rewrites and SMS-based login flow. Text blast endpoint implemented with confirmation and validation, integrating with the discovered API.

Changes

Cohort / File(s) Summary
Authentication & Blast Documentation
docs/research/2026-03-24-auth-flow-endpoints.md, docs/research/2026-03-24-text-blast-endpoint.md
New research documents detailing authentication endpoint flows (SMS code, Firebase token exchange) and text blast API specifications including request/response structure, validation rules, and discovered endpoints.
CLI Infrastructure
src/cli.js
Added pre-parse argument rewriting to map deprecated top-level subcommands (list, get, cancel, clone) to their newer nested paths with deprecation warnings. Special-case handling for guests <id> rewrite to guests list <id>.
Auth Command Implementation
src/commands/auth.js
Replaced HTTP server/bookmarklet login flow with SMS verification-code flow. Added platform-specific auto-retrieval (macOS imsg, Android termux-sms-list), API helpers for code submission and token exchange, phone validation, and interactive code entry fallback. Command now requires <phone> argument with --code and --no-auto options.
Blasts Command Implementation
src/commands/blasts.js
Implemented real POST /createTextBlast endpoint integration with message validation (480-char limit), comma-separated guest status targeting, event page visibility toggle, dry-run mode, and interactive confirmation. Added structured error handling and JSON status output.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant Partiful as Partiful API
    participant Firebase as Firebase API
    participant SMS as SMS System

    User->>Partiful: POST /sendAuthCodeTrusted<br/>(phone, display name, channel)
    Partiful->>SMS: Send SMS with auth code
    SMS->>User: Code received via SMS

    alt Auto-Retrieval Available
        User->>User: Platform-specific retrieval<br/>(imsg or termux-sms-list)
    else Manual Entry
        User->>User: User enters code interactively
    end

    User->>Partiful: POST /getLoginToken<br/>(phone, auth code)
    Partiful->>User: Returns custom Firebase token

    User->>Firebase: POST /accounts:signInWithCustomToken<br/>(custom token)
    Firebase->>User: Returns idToken, refreshToken, expiresIn

    opt User Lookup
        User->>Firebase: POST /accounts:lookup<br/>(idToken)
        Firebase->>User: User profile data
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A whisker-twitch of auth redrawn,
SMS codes hop through the dawn,
Firebase tokens dance and spin,
Text blasts confirmed—let joy begin!
CLI commands dusted clean,
The finest CLI you've seen! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: replacing bookmarklet-based login with phone-number authentication featuring auto SMS code retrieval.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sasha/phone-auth-flow

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (7)
src/commands/auth.js (3)

306-310: Document the US-default assumption more prominently.

Automatically prepending +1 for numbers without a country code assumes US users. Consider logging this assumption to stderr when applied so international users understand why their number was modified.

📝 Suggested improvement
         if (!phoneNumber.startsWith('+')) {
           // Assume US if no country code
           phoneNumber = '+1' + phoneNumber;
+          console.error(`Note: No country code provided, assuming US (+1${phone.replace(/[\s\-\(\)]/g, '')})`);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/auth.js` around lines 306 - 310, The code currently strips
formatting into phoneNumber and blindly prepends '+1' when it doesn't start with
'+'; update the logic around the phoneNumber assignment (the branch that adds
'+1') to write a clear stderr warning when this US-default assumption is applied
(e.g., using console.error or process.stderr.write) so callers see their number
was modified; keep the same normalization (phone.replace(/[\s\-\(\)]/g, '')) and
only emit the message inside the if (!phoneNumber.startsWith('+')) block and
include both the original phone and the normalized phoneNumber in the message
for clarity.

59-66: which command is Unix-specific.

While the current code doesn't call hasCommand() on Windows (the platform check returns early), consider adding a comment or guard for future maintainability if this function is reused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/auth.js` around lines 59 - 66, The helper hasCommand(cmd) uses
the Unix-only `which` binary which is not available on Windows; update
hasCommand to either include a clear comment noting it is Unix-specific and must
not be used on Windows, or add a cross-platform guard that detects
process.platform === 'win32' and uses an appropriate Windows check (or returns
false) before running execSync; reference the hasCommand function when making
this change so future callers know it's platform-specific or has a Windows-safe
path.

77-89: Handle empty or malformed imsg output gracefully.

If imsg chats returns empty output, chatsRaw.trim().split('\n').map(line => JSON.parse(line)) will attempt to parse an empty string, throwing an error. While the outer catch silently continues polling, this could mask other issues.

🛡️ Suggested defensive fix
       const chatsRaw = execSync('imsg chats --limit 30 --json', { encoding: 'utf8', timeout: 10000 });
-      const chats = chatsRaw.trim().split('\n').map(line => JSON.parse(line));
+      const chats = chatsRaw.trim().split('\n').filter(line => line).map(line => JSON.parse(line));

       const partifulChat = chats.find(c =>
         c.identifier === PARTIFUL_SMS_SENDER ||
         c.identifier?.includes('8449460698')
       );

       if (partifulChat) {
         const historyRaw = execSync(`imsg history --chat-id ${partifulChat.id} --limit 3 --json`, {
           encoding: 'utf8', timeout: 10000
         });
-        const messages = historyRaw.trim().split('\n').map(line => JSON.parse(line));
+        const messages = historyRaw.trim().split('\n').filter(line => line).map(line => JSON.parse(line));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/auth.js` around lines 77 - 89, The code assumes imsg output is
non-empty and JSON-valid; guard parsing of both chatsRaw and historyRaw by first
checking that chatsRaw.trim() (and historyRaw.trim()) is non-empty, filter out
empty lines before mapping, and safely parse each line (or skip lines that fail
JSON.parse) to avoid throwing on empty/malformed output; additionally validate
that parsed entries contain expected fields (e.g., c.id, c.identifier) before
using them and add a short error/log on parse failure so execSync failures or
malformed imsg output are visible (apply these changes around the chatsRaw→chats
and historyRaw→messages handling, and keep using partifulChat and its id only
after validation).
docs/research/2026-03-24-auth-flow-endpoints.md (1)

13-15: Add language specifiers to endpoint URL code blocks.

Several fenced code blocks for endpoint URLs lack language specifiers. Adding text or http would satisfy the markdownlint rule and improve consistency.

📝 Example fix for line 13
-```
+```text
 POST https://api.partiful.com/sendAuthCode

Apply similar changes to lines 41, 74, 97, and 112.

</details>




Also applies to: 41-43, 74-76, 97-99, 112-114

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/research/2026-03-24-auth-flow-endpoints.md around lines 13 - 15, The
fenced code blocks containing plain endpoint URLs (e.g., "POST
https://api.partiful.com/sendAuthCode", "POST
https://api.partiful.com/verifyAuthCode", "GET
https://api.partiful.com/authStatus", "POST
https://api.partiful.com/resendAuthCode", and "DELETE
https://api.partiful.com/revokeAuth") need a language specifier to satisfy
markdownlint; update each triple-backtick fence that wraps those URL lines to
include a short specifier like text (or http) immediately after the
opening backticks so the blocks become text ... for all instances
referenced in the diff.


</details>

</blockquote></details>
<details>
<summary>docs/research/2026-03-24-text-blast-endpoint.md (1)</summary><blockquote>

`11-13`: **Add language specifier to fenced code block.**

The endpoint URL code block should specify a language for consistency with other code blocks in the document.

<details>
<summary>📝 Suggested fix</summary>

```diff
-```
+```text
 POST https://api.partiful.com/createTextBlast
 ```
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/research/2026-03-24-text-blast-endpoint.md around lines 11 - 13, The
fenced code block containing the endpoint URL (the block with "POST
https://api.partiful.com/createTextBlast") should include a language specifier
for consistency; update that block to use a text fence (e.g., change ``` to

src/cli.js (1)

48-60: Minor inconsistency in deprecation message for clone alias.

For the clone alias, newArgs.slice(0, 2) yields ['+clone'] (single element), while other aliases yield two elements like ['events', 'list']. The message still works but is slightly inconsistent in format.

Consider adjusting for clarity:

🔧 Suggested improvement
-    process.stderr.write(`[deprecated] "partiful ${args[0]}" → use "partiful ${newArgs.slice(0, 2).join(' ')}" instead\n`);
+    process.stderr.write(`[deprecated] "partiful ${args[0]}" → use "partiful ${newArgs[0]}${newArgs.length > 1 ? ' ' + newArgs[1] : ''}" instead\n`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli.js` around lines 48 - 60, The deprecation message is inconsistent for
the 'clone' alias because aliasMap['clone'] is ['+clone'] so newArgs.slice(0,2)
yields a single token; update the deprecation message logic around
process.stderr.write to handle single-token aliases (aliasMap / newArgs /
newArgs.slice) by formatting the suggested usage robustly (e.g., if newArgs has
only one element, print that element alone or prepend the default command
namespace so the message always looks like "partiful <command> <subcommand>").
Ensure the change touches the aliasMap handling and the process.stderr.write
call so the message is consistently two-part or clearly formatted for
single-part aliases.
src/commands/blasts.js (1)

17-17: Unused constant.

MAX_BLASTS_PER_EVENT is defined but never used in the code. Either implement the client-side limit check or remove the constant to avoid dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/blasts.js` at line 17, MAX_BLASTS_PER_EVENT is defined but never
used; either remove this dead constant or enforce it where blasts are
validated/sent by checking the current event's blast count against
MAX_BLASTS_PER_EVENT before creating/sending new blasts (e.g., in the routine
that validates/creates blasts), returning a clear client error message when the
limit is reached and updating related tests and messages accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/blasts.js`:
- Around line 110-116: The catch block is accessing a non-existent err.code on
PartifulError; update the error handling in the catch inside the blasts command
so when err instanceof PartifulError you pass err.type (the actual property on
PartifulError) to jsonError instead of err.code, preserving the other args
(err.message, err.exitCode, err.details) and keeping the fallback branch
unchanged.

---

Nitpick comments:
In `@docs/research/2026-03-24-auth-flow-endpoints.md`:
- Around line 13-15: The fenced code blocks containing plain endpoint URLs
(e.g., "POST https://api.partiful.com/sendAuthCode", "POST
https://api.partiful.com/verifyAuthCode", "GET
https://api.partiful.com/authStatus", "POST
https://api.partiful.com/resendAuthCode", and "DELETE
https://api.partiful.com/revokeAuth") need a language specifier to satisfy
markdownlint; update each triple-backtick fence that wraps those URL lines to
include a short specifier like ```text (or ```http) immediately after the
opening backticks so the blocks become ```text ... ``` for all instances
referenced in the diff.

In `@docs/research/2026-03-24-text-blast-endpoint.md`:
- Around line 11-13: The fenced code block containing the endpoint URL (the
block with "POST https://api.partiful.com/createTextBlast") should include a
language specifier for consistency; update that block to use a text fence (e.g.,
change ``` to ```text) so the code block header reads like the other examples in
the document.

In `@src/cli.js`:
- Around line 48-60: The deprecation message is inconsistent for the 'clone'
alias because aliasMap['clone'] is ['+clone'] so newArgs.slice(0,2) yields a
single token; update the deprecation message logic around process.stderr.write
to handle single-token aliases (aliasMap / newArgs / newArgs.slice) by
formatting the suggested usage robustly (e.g., if newArgs has only one element,
print that element alone or prepend the default command namespace so the message
always looks like "partiful <command> <subcommand>"). Ensure the change touches
the aliasMap handling and the process.stderr.write call so the message is
consistently two-part or clearly formatted for single-part aliases.

In `@src/commands/auth.js`:
- Around line 306-310: The code currently strips formatting into phoneNumber and
blindly prepends '+1' when it doesn't start with '+'; update the logic around
the phoneNumber assignment (the branch that adds '+1') to write a clear stderr
warning when this US-default assumption is applied (e.g., using console.error or
process.stderr.write) so callers see their number was modified; keep the same
normalization (phone.replace(/[\s\-\(\)]/g, '')) and only emit the message
inside the if (!phoneNumber.startsWith('+')) block and include both the original
phone and the normalized phoneNumber in the message for clarity.
- Around line 59-66: The helper hasCommand(cmd) uses the Unix-only `which`
binary which is not available on Windows; update hasCommand to either include a
clear comment noting it is Unix-specific and must not be used on Windows, or add
a cross-platform guard that detects process.platform === 'win32' and uses an
appropriate Windows check (or returns false) before running execSync; reference
the hasCommand function when making this change so future callers know it's
platform-specific or has a Windows-safe path.
- Around line 77-89: The code assumes imsg output is non-empty and JSON-valid;
guard parsing of both chatsRaw and historyRaw by first checking that
chatsRaw.trim() (and historyRaw.trim()) is non-empty, filter out empty lines
before mapping, and safely parse each line (or skip lines that fail JSON.parse)
to avoid throwing on empty/malformed output; additionally validate that parsed
entries contain expected fields (e.g., c.id, c.identifier) before using them and
add a short error/log on parse failure so execSync failures or malformed imsg
output are visible (apply these changes around the chatsRaw→chats and
historyRaw→messages handling, and keep using partifulChat and its id only after
validation).

In `@src/commands/blasts.js`:
- Line 17: MAX_BLASTS_PER_EVENT is defined but never used; either remove this
dead constant or enforce it where blasts are validated/sent by checking the
current event's blast count against MAX_BLASTS_PER_EVENT before creating/sending
new blasts (e.g., in the routine that validates/creates blasts), returning a
clear client error message when the limit is reached and updating related tests
and messages accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c0202ca-3168-4be8-aac6-4afef173d921

📥 Commits

Reviewing files that changed from the base of the PR and between 9d30134 and e42362e.

📒 Files selected for processing (5)
  • docs/research/2026-03-24-auth-flow-endpoints.md
  • docs/research/2026-03-24-text-blast-endpoint.md
  • src/cli.js
  • src/commands/auth.js
  • src/commands/blasts.js

- cli.js: alias rewrite scans past global opts, fix clone mapping
- blasts.js: err.code → err.type, jsonOutput opts, userId fallback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant