Skip to content

Latest commit

 

History

History
177 lines (134 loc) · 4.69 KB

File metadata and controls

177 lines (134 loc) · 4.69 KB
title Scripts And App Passwords
description Use Poptart from trusted scripts, cron jobs, and internal tools with app-password sessions.

For scripts and internal tools that you run yourself, app-password sessions are the simplest way to call authenticated AT Protocol endpoints. They avoid browser redirect handling and work well with generated descriptors.

Use this flow for cron jobs, moderation helpers, migration scripts, and local admin tools. Use OAuth instead for user-facing apps.

Install

dart pub add poptart
import 'package:poptart/poptart.dart';

Read Credentials From The Environment

Do not hard-code credentials in the script. Read the identifier and app password from environment variables or your secret manager.

import 'dart:io';

String requiredEnv(String name) {
  final value = Platform.environment[name];
  if (value == null || value.isEmpty) {
    throw StateError('Missing required environment variable: $name');
  }
  return value;
}

Run the script with values like:

BSKY_IDENTIFIER=alice.example.com BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx dart run bin/tool.dart

Create A Session

Use createSession to exchange the identifier and app password for an AT Protocol session.

Future<Session> createScriptSession() async {
  final response = await createSession(
    identifier: requiredEnv('BSKY_IDENTIFIER'),
    password: requiredEnv('BSKY_APP_PASSWORD'),
  );

  return response.data;
}

The returned Session includes the DID, handle, access JWT, and refresh JWT.

Call Generated Lexicons

Generated method values and descriptors keep the script typed without making it verbose.

import 'dart:io';

import 'package:poptart/poptart.dart';
import 'package:bluesky_poptart/app/bsky/notification/list_notifications.dart'
    as notifications;

Future<void> main() async {
  final session = await createScriptSession();
  final client = PoptartClient.fromSession(session);

  final response = await client.call(
    notifications.appBskyNotificationListNotifications,
    parameters: const notifications.NotificationListNotificationsInput(
      limit: 50,
    ),
  );

  for (final notification in response.data.notifications) {
    stdout.writeln(
      '${notification.reason}: ${notification.author.handle}',
    );
  }
}

Prefer generated lexicon calls for known methods. The named method value is the friendly path; notifications.methodDescriptor is equally valid when the descriptor reads better in a script. Both give typed parameters, typed output, and less response-shape guessing.

Create A Record

Repo writes use the same authenticated client. The generated method decides which lexicon method is called.

import 'package:poptart/poptart.dart';
import 'package:bluesky_poptart/app/bsky/feed/post.dart' as feed_post;
import 'package:poptart_lex/com/atproto/repo/create_record.dart'
    as create_record;

Future<void> publishPost(Session session, String text) async {
  final client = PoptartClient.fromSession(session);

  final post = feed_post.FeedPostRecord(
    text: text,
    createdAt: DateTime.now().toUtc(),
  );

  await client.call(
    create_record.comAtprotoRepoCreateRecord,
    input: create_record.RepoCreateRecordInput(
      repo: session.did,
      collection: 'app.bsky.feed.post',
      record: post.toJson(),
    ),
  );
}

Build records with generated models when possible, then pass toJson() to repo write inputs.

Refresh Long-Running Sessions

For short scripts, create a fresh session each run. For longer-running jobs, refresh the session before the access token expires.

Future<Session> refreshScriptSession(Session session) async {
  final refreshed = await refreshSession(refreshJwt: session.refreshJwt);
  return refreshed.data;
}

If your process keeps running for hours, store the latest refresh JWT after each refresh and replace the in-memory Session.

Error Handling

Wrap the top-level script so failures are explicit and exit non-zero.

Future<void> main() async {
  try {
    final session = await createScriptSession();
    await publishPost(session, 'Hello from a Poptart script');
  } catch (error, stack) {
    stderr.writeln(error);
    stderr.writeln(stack);
    exitCode = 1;
  }
}

For scheduled jobs, log enough context to know which account and method failed, but never log app passwords, access JWTs, refresh JWTs, or OAuth private keys.

When Not To Use This Flow

Do not ask users to type their password or app password into a shipped app. Use OAuth for that.

Do not use account passwords for scripts. Create an app password with only the access needed for the job.

Do not store generated sessions in world-readable files. Treat session material as credentials.