Status::class, 'nodes' => 'array', 'connections' => 'array', 'activated_at' => 'datetime', 'paused_at' => 'datetime', ]; protected static function booted(): void { static::saving(function (self $automation): void { $automation->nodes = self::encryptSensitiveFields( $automation->nodes ?? [], $automation->getOriginal('nodes') ?? [], ); }); } public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function user(): BelongsTo { return $this->belongsTo(User::class); } public function triggerItems(): HasMany { return $this->hasMany(AutomationTriggerItem::class); } public function runs(): HasMany { return $this->hasMany(AutomationRun::class); } /** * Walks both the incoming and stored node lists and reconciles sensitive * fields: a PLACEHOLDER value means "user didn't change it" (frontend * never received the real value) so we keep the existing ciphertext. Plain * text values get encrypted; already-encrypted strings pass through. * * @param array> $incoming * @param array>|string $original * @return array> */ private static function encryptSensitiveFields(array $incoming, array|string $original): array { $original = is_array($original) ? $original : (json_decode($original, true) ?: []); $originalById = collect($original)->keyBy('id'); foreach ($incoming as &$node) { $originalNode = $originalById->get($node['id'] ?? null); foreach (self::SENSITIVE_NODE_FIELDS as $field) { $value = data_get($node, "data.{$field}"); if (! is_string($value) || $value === '') { continue; } if ($value === self::SENSITIVE_PLACEHOLDER) { data_set($node, "data.{$field}", data_get($originalNode, "data.{$field}", '')); continue; } if (self::looksEncrypted($value)) { continue; } data_set($node, "data.{$field}", Crypt::encryptString($value)); } } return $incoming; } /** * Quick check for Laravel's `Crypt::encryptString` output without paying * the cost of a full decrypt attempt. Laravel wraps payloads as base64 * JSON beginning with the canonical `eyJpdiI` ("{"iv":"...) prefix. */ private static function looksEncrypted(string $value): bool { if (! str_starts_with($value, 'eyJ')) { return false; } try { Crypt::decryptString($value); return true; } catch (Throwable) { return false; } } }