Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ Continue to [Validation](#validation), [Import](#importing-users), and [Post-Imp

## Migrating from Firebase Auth

The recommended path is `transform-firebase --package`, which writes a [migration package](docs/migration-package.md) ready for `import-package`. The legacy `--output <csv>` mode still ships for back-compat.

### 1. Export from Firebase

Export your users from the Firebase Console or using the Firebase CLI (`firebase auth:export`). This produces a JSON file with a `users` array.
Expand All @@ -262,6 +264,22 @@ If you want to migrate passwords, get the hash parameters from Firebase Console

### 3. Transform to WorkOS format

Migration package (recommended):

```bash
workos-migrate transform-firebase \
--input firebase-export.json \
--package \
--output-dir ./migration-firebase \
--signer-key <BASE64_KEY> \
--salt-separator <BASE64_SEP> \
--rounds 8 \
--memory-cost 14 \
--org-mapping orgs.csv
```

Legacy single CSV (still supported):

```bash
workos-migrate transform-firebase \
--input firebase-export.json \
Expand All @@ -274,6 +292,9 @@ workos-migrate transform-firebase \

Options:

- `--package` - Write a migration package instead of a single CSV.
- `--output-dir <dir>` - Required when `--package` is set.
- `--source-tenant <name>` - Optional tenant identifier recorded in the manifest.
- `--name-split <strategy>` - How to split `displayName` into first/last: `first-space` (default), `last-space`, or `first-name-only`
- `--include-disabled` - Include disabled users (excluded by default)
- `--skip-passwords` - Skip password hash encoding
Expand Down
40 changes: 38 additions & 2 deletions dist/cli/commands/transform-firebase.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import fs from 'node:fs';
import chalk from 'chalk';
import { transformFirebaseExport } from '../../transformers/firebase/transformer.js';
import { exportFirebasePackage } from '../../transformers/firebase/package-exporter.js';
export function registerTransformFirebaseCommand(program) {
program
.command('transform-firebase')
.description('Transform Firebase Auth JSON export to WorkOS-compatible CSV')
.description('Transform Firebase Auth JSON export to WorkOS-compatible CSV or migration package')
.requiredOption('--input <path>', 'Firebase Auth JSON export file')
.requiredOption('--output <path>', 'Output WorkOS CSV file')
.option('--output <path>', 'Output WorkOS CSV file (legacy single-CSV mode)')
.option('--package', 'Write a migration package instead of a single CSV')
.option('--output-dir <dir>', 'Output directory when --package is set')
.option('--source-tenant <name>', 'Optional source tenant identifier to record in the manifest')
.option('--org-mapping <path>', 'Org mapping CSV (firebase_uid,org_external_id,org_name)')
.option('--role-mapping <path>', 'Role mapping CSV (firebase_uid,role_slug)')
.option('--include-disabled', 'Include disabled users (excluded by default)')
Expand Down Expand Up @@ -41,6 +45,38 @@ export function registerTransformFirebaseCommand(program) {
memoryCost: parseInt(opts.memoryCost, 10),
};
}
if (opts.package) {
if (!opts.outputDir) {
console.error(chalk.red('--output-dir is required when --package is set'));
process.exit(1);
}
const stats = await exportFirebasePackage({
input: opts.input,
outputDir: opts.outputDir,
scryptConfig,
nameSplitStrategy: opts.nameSplit,
includeDisabled: opts.includeDisabled ?? false,
skipPasswords: opts.skipPasswords ?? false,
orgMapping: opts.orgMapping,
roleMapping: opts.roleMapping,
sourceTenant: opts.sourceTenant,
quiet: opts.quiet ?? false,
});
if (!opts.quiet) {
console.log(chalk.green('\nFirebase package export complete'));
console.log(` Users: ${stats.totalUsers}`);
console.log(` Orgs: ${stats.totalOrgs}`);
console.log(` Memberships: ${stats.totalMemberships}`);
console.log(` Roles: ${stats.roleDefinitions}`);
console.log(` Skipped: ${stats.skippedUsers}`);
console.log(` Warnings: ${stats.warnings.length}`);
}
return;
}
if (!opts.output) {
console.error(chalk.red('--output is required unless --package is set'));
process.exit(1);
}
const startTime = Date.now();
if (!opts.quiet) {
console.log(chalk.blue('Transforming Firebase export...'));
Expand Down
22 changes: 5 additions & 17 deletions dist/exporters/auth0/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ export async function exportAuth0CsvWithClient(client, options) {
}
const startTime = Date.now();
const warnings = [];
let totalUsers = 0;
let totalOrgs = 0;
let skippedUsers = 0;
// Open output streams
const output = options.output;
const writeStream = createWriteStream(output, { encoding: 'utf-8' });
Expand All @@ -56,18 +53,10 @@ export async function exportAuth0CsvWithClient(client, options) {
try {
// Write CSV header
writeStream.write(CSV_COLUMNS.join(',') + '\n');
if (options.useMetadata) {
const stats = await exportUsersWithMetadata(client, writeStream, skippedStream, options, warnings);
totalUsers = stats.totalUsers;
totalOrgs = stats.totalOrgs;
skippedUsers = stats.skippedUsers;
}
else {
const stats = await exportOrganizations(client, writeStream, skippedStream, options, warnings);
totalUsers = stats.totalUsers;
totalOrgs = stats.totalOrgs;
skippedUsers = stats.skippedUsers;
}
const stats = options.useMetadata
? await exportUsersWithMetadata(client, writeStream, skippedStream, options, warnings)
: await exportOrganizations(client, writeStream, skippedStream, options, warnings);
const { totalUsers, totalOrgs, skippedUsers } = stats;
await closeStream(writeStream);
await closeStream(skippedStream);
const duration = Date.now() - startTime;
Expand All @@ -91,11 +80,10 @@ export async function exportAuth0CsvWithClient(client, options) {
}
async function exportOrganizations(client, writeStream, skippedStream, options, warnings) {
let totalUsers = 0;
let totalOrgs = 0;
let skippedUsers = 0;
// Fetch all organizations
const organizations = await fetchAllOrganizations(client, options.pageSize);
totalOrgs = organizations.length;
const totalOrgs = organizations.length;
if (!options.quiet) {
logger.info(`Found ${totalOrgs} organizations`);
}
Expand Down
2 changes: 1 addition & 1 deletion dist/import/org-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class OrgCache {
}
if (!resolvedOrgId) {
throw new Error(`Organization with external_id "${orgExternalId}" reported as existing ` +
`but could not be retrieved after 3 retries. Original: ${errorMsg}`);
`but could not be retrieved after 3 retries. Original: ${errorMsg}`, { cause: err });
}
}
else {
Expand Down
1 change: 0 additions & 1 deletion dist/roles/api-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export async function listRolesForOrganization(organizationId) {
const apiKey = getApiKey();
const roles = [];
let after;
// eslint-disable-next-line no-constant-condition
while (true) {
const params = new URLSearchParams({ limit: '100' });
if (after)
Expand Down
40 changes: 40 additions & 0 deletions dist/transformers/firebase/package-exporter.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { FirebaseScryptConfig, NameSplitStrategy } from '../../shared/types.js';
export interface FirebasePackageExportOptions {
input: string;
outputDir: string;
scryptConfig?: FirebaseScryptConfig;
nameSplitStrategy: NameSplitStrategy;
includeDisabled?: boolean;
skipPasswords?: boolean;
orgMapping?: string;
roleMapping?: string;
sourceTenant?: string;
quiet?: boolean;
}
export interface FirebasePackageWarning {
timestamp: string;
code: string;
message: string;
firebase_uid?: string;
email?: string;
}
export interface FirebasePackageSkipped {
timestamp: string;
firebase_uid?: string;
email?: string;
reason: string;
}
export interface FirebasePackageStats {
totalUsers: number;
totalOrgs: number;
totalMemberships: number;
roleDefinitions: number;
userRoleAssignments: number;
uploadUsers: number;
uploadOrganizations: number;
uploadMemberships: number;
skippedUsers: number;
warnings: FirebasePackageWarning[];
skipped: FirebasePackageSkipped[];
}
export declare function exportFirebasePackage(options: FirebasePackageExportOptions): Promise<FirebasePackageStats>;
Loading