import { readFile, writeFile } from 'node:fs/promises'; type Fields = Record; type Method = [string, Fields, string]; type Result = { methodResponses: Method[] }; export async function bootstrap(runtime: string, store: Fields, fixture: { issuer: string; origins: string[]; management: number }): Promise { const token = (await readFile(`${runtime}/bootstrap.pat`, 'utf8')).trim(); async function idp(path: string, body?: Fields, method = 'POST'): Promise { const response = await fetch(`${fixture.issuer}${path}`, { method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); const value = await response.json() as Fields; if (!response.ok) throw new Error(`${path}: ${JSON.stringify(value)}`); return value; } async function mail(method: string, args: Fields): Promise { const response = await fetch(`http://localhost:${fixture.management}/jmap/`, { method: 'POST', headers: { Authorization: `Basic ${Buffer.from('synthetic-admin:synthetic-admin').toString('base64')}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ using: ['urn:stalwart:jmap'], methodCalls: [[method, args, 't']] }), }); const value = await response.json() as Result; const result = value.methodResponses[0]; if (result[0] === 'error' || result[1].notCreated || result[1].notUpdated) throw new Error(`${method}: ${JSON.stringify(value)}`); return result[1]; } async function create(name: string, value: Fields): Promise { const result = await mail(`x:${name}/set`, { create: { test: value } }); return (result.created as Record).test.id; } async function singleton(name: string, value: Fields): Promise { await mail(`x:${name}/set`, { update: { singleton: value } }); } const project = await idp('/management/v1/projects', { name: 'Mail keys', projectRoleAssertion: true, projectRoleCheck: true, hasProjectCheck: true }); const projectId = String(project.id); await idp(`/management/v1/projects/${projectId}/roles`, { roleKey: 'mail_user', displayName: 'Mail user', group: 'Mail' }); const app = await idp(`/management/v1/projects/${projectId}/apps/oidc`, { name: 'Mail keys', redirectUris: fixture.origins.map(origin => `${origin}/account/oauth/callback`), responseTypes: ['OIDC_RESPONSE_TYPE_CODE'], grantTypes: ['OIDC_GRANT_TYPE_AUTHORIZATION_CODE'], appType: 'OIDC_APP_TYPE_USER_AGENT', authMethodType: 'OIDC_AUTH_METHOD_TYPE_NONE', postLogoutRedirectUris: fixture.origins.map(origin => `${origin}/account/oauth/logout`), version: 'OIDC_VERSION_1_0', devMode: true, accessTokenType: 'OIDC_TOKEN_TYPE_BEARER', idTokenUserinfoAssertion: true, }); const users: Record = {}; for (const name of ['alice', 'bob']) { const password = `Synthetic-${name}-42!`; const user = await idp('/management/v1/users/human', { userName: `${name}@example.test`, profile: { firstName: name, lastName: 'Synthetic', preferredLanguage: 'en' }, email: { email: `${name}@example.test`, isEmailVerified: true }, initialPassword: password, }); const id = String(user.userId); await idp(`/management/v1/users/${id}/password`, { password, noChangeRequired: true }); await idp(`/management/v1/users/${id}/grants`, { projectId, roleKeys: ['mail_user'] }); users[name] = { id, password }; } const domain = await create('Domain', { name: 'example.test', certificateManagement: { '@type': 'Manual' }, dkimManagement: { '@type': 'Manual' }, dnsManagement: { '@type': 'Manual' } }); const authDomain = await create('Domain', { name: 'auth.example.test', certificateManagement: { '@type': 'Manual' }, dkimManagement: { '@type': 'Manual' }, dnsManagement: { '@type': 'Manual' } }); const protocolPerms = ['authenticate', 'emailSend', 'emailReceive', 'imapAuthenticate', 'imapAppend', 'imapCapability', 'imapId', 'imapCopy', 'imapMove', 'imapCreate', 'imapDelete', 'imapEnable', 'imapExpunge', 'imapFetch', 'imapIdle', 'imapList', 'imapLsub', 'imapNamespace', 'imapRename', 'imapSearch', 'imapSort', 'imapSelect', 'imapExamine', 'imapStatus', 'imapStore', 'imapSubscribe', 'imapThread']; const permissions = Object.fromEntries([...protocolPerms, 'authenticateWithAlias', 'sysAppPasswordGet', 'sysAppPasswordCreate', 'sysAppPasswordUpdate', 'sysAppPasswordDestroy', 'sysAppPasswordQuery'].map(name => [name, true])); for (const [name, user] of Object.entries(users)) { await create('Account', { '@type': 'User', name, domainId: domain, aliases: { '0': { name: user.id, domainId: authDomain } }, credentials: {}, roles: { '@type': 'User' }, permissions: { '@type': 'Replace', enabledPermissions: permissions, disabledPermissions: {} }, memberGroupIds: {}, encryptionAtRest: { '@type': 'Disabled' }, quotas: {}, }); } const directory = await create('Directory', { '@type': 'Oidc', description: 'Synthetic Zitadel', issuerUrl: fixture.issuer, requireAudience: projectId, requireScopes: {}, claimUsername: 'sub', usernameDomain: 'auth.example.test', claimName: 'sub', claimGroups: null, }); await singleton('Authentication', { directoryId: directory }); await singleton('DataStore', store); await singleton('SearchStore', store); await singleton('BlobStore', { '@type': 'Default' }); await singleton('Http', { useXForwarded: true }); await singleton('SystemSettings', { defaultHostname: 'mail.example.test', defaultDomainId: domain }); await create('Certificate', { certificate: { '@type': 'File', filePath: '/fixture/tls.crt' }, privateKey: { '@type': 'File', filePath: '/fixture/tls.key' } }); await create('NetworkListener', { name: 'http', bind: { '0.0.0.0:18882': true }, protocol: 'http', useTls: false, tlsImplicit: false }); await create('NetworkListener', { name: 'imap', bind: { '0.0.0.0:18143': true }, protocol: 'imap', useTls: true, tlsImplicit: true }); await create('NetworkListener', { name: 'submission', bind: { '0.0.0.0:18587': true }, protocol: 'smtp', useTls: true, tlsImplicit: true }); await create('NetworkListener', { name: 'account', bind: { '127.0.0.1:8081': true }, protocol: 'http', useTls: false, tlsImplicit: false }); await create('Tracer', { '@type': 'Stdout', buffered: false, ansi: false, multiline: false, enable: true, level: 'info', events: {}, eventsPolicy: 'exclude' }); await writeFile(`${runtime}/config.json`, JSON.stringify({ issuer: fixture.issuer, clientId: app.clientId, scope: `openid profile email urn:zitadel:iam:org:project:id:${projectId}:aud` })); await writeFile(`${runtime}/users.json`, JSON.stringify(users)); console.log('Synthetic Zitadel users and least-privilege Stalwart accounts configured.'); }